Compare commits

..

10 Commits

11 changed files with 1584 additions and 225 deletions

467
Cargo.lock generated
View File

@ -375,6 +375,27 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -527,6 +548,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "combine"
version = "4.6.6"
@ -605,6 +632,40 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset 0.8.0",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.15"
@ -614,6 +675,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -785,6 +852,12 @@ dependencies = [
"web-sys",
]
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "emath"
version = "0.21.0"
@ -879,6 +952,22 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "exr"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4"
dependencies = [
"bit_field",
"flume",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "fastrand"
version = "1.9.0"
@ -898,6 +987,25 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.10.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"pin-project",
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
@ -1016,8 +1124,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "gif"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045"
dependencies = [
"color_quant",
"weezl",
]
[[package]]
name = "git-version"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899"
dependencies = [
"git-version-macro",
"proc-macro-hack",
]
[[package]]
name = "git-version-macro"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f"
dependencies = [
"proc-macro-hack",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
@ -1107,6 +1249,15 @@ dependencies = [
"gl_generator",
]
[[package]]
name = "half"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@ -1144,6 +1295,25 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "image"
version = "0.24.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"jpeg-decoder",
"num-rational",
"num-traits",
"png",
"qoi",
"tiff",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@ -1214,6 +1384,15 @@ dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
dependencies = [
"rayon",
]
[[package]]
name = "js-sys"
version = "0.3.61"
@ -1258,6 +1437,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.140"
@ -1300,6 +1485,21 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "line-wrap"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
@ -1373,6 +1573,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "memoffset"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
dependencies = [
"autocfg",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1400,6 +1609,15 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom",
]
[[package]]
name = "ndk"
version = "0.7.0"
@ -1484,6 +1702,36 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.15.0"
@ -1576,6 +1824,28 @@ version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "openssl-sys"
version = "0.9.83"
@ -1686,6 +1956,26 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pin-project"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
@ -1704,6 +1994,20 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "plist"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
dependencies = [
"base64",
"indexmap",
"line-wrap",
"quick-xml",
"serde",
"time",
]
[[package]]
name = "png"
version = "0.17.7"
@ -1748,6 +2052,12 @@ dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.53"
@ -1757,6 +2067,24 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-xml"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c1a97b1bc42b1d550bfb48d4262153fe400a12bab1511821736f7eac76d7e2"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.26"
@ -1802,6 +2130,28 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f851a03551ceefd30132e447f07f96cb7011d6b658374f3aed847333adb5559"
[[package]]
name = "rayon"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
@ -1882,6 +2232,12 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "safemem"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
[[package]]
name = "same-file"
version = "1.0.6"
@ -2015,6 +2371,12 @@ dependencies = [
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f"
[[package]]
name = "slab"
version = "0.4.8"
@ -2095,6 +2457,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "ssh2"
version = "0.9.4"
@ -2147,6 +2518,29 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syntect"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8"
dependencies = [
"bincode",
"bitflags",
"flate2",
"fnv",
"lazy_static",
"once_cell",
"onig",
"plist",
"regex-syntax",
"serde",
"serde_derive",
"serde_json",
"thiserror",
"walkdir",
"yaml-rust",
]
[[package]]
name = "tempfile"
version = "3.4.0"
@ -2180,6 +2574,44 @@ dependencies = [
"syn 2.0.10",
]
[[package]]
name = "tiff"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
dependencies = [
"flate2",
"jpeg-decoder",
"weezl",
]
[[package]]
name = "time"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
dependencies = [
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "time-macros"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
dependencies = [
"time-core",
]
[[package]]
name = "tiny-skia"
version = "0.8.3"
@ -2331,16 +2763,21 @@ dependencies = [
"directories-next",
"eframe",
"egui",
"git-version",
"hex",
"image",
"lazy-regex",
"lazy_static",
"serde",
"serde_json",
"shell-escape",
"smol",
"ssh2",
"syntect",
"thiserror",
"tokio",
"toml",
"version",
]
[[package]]
@ -2397,6 +2834,12 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a449064fee414fcc201356a3e6c1510f6c8829ed28bb06b91c54ebe208ce065"
[[package]]
name = "version_check"
version = "0.9.4"
@ -2603,6 +3046,12 @@ dependencies = [
"web-sys",
]
[[package]]
name = "weezl"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
[[package]]
name = "winapi"
version = "0.3.9"
@ -2843,6 +3292,15 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zbus"
version = "3.11.1"
@ -2908,6 +3366,15 @@ dependencies = [
"zvariant",
]
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zvariant"
version = "3.12.0"

View File

@ -11,13 +11,18 @@ async-ssh2-lite = { version = "0.4.5", features = ["async-io"] }
directories-next = "2.0.0"
eframe = "0.21.3"
egui = "0.21.0"
git-version = "0.3.5"
hex = "0.4.3"
image = "0.24.6"
lazy-regex = "2.5.0"
lazy_static = "1.4.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"
syntect = "5.0.0"
thiserror = "1.0.40"
tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] }
toml = "0.7.3"
version = "3.0.0"

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# ubusman
Kind of like [postman](https://www.postman.com/), but for [ubus](https://openwrt.org/docs/techref/ubus).

View File

@ -1,21 +1,602 @@
use std::{
net::{SocketAddr, SocketAddrV4},
sync::{mpsc::{Receiver, Sender}, Arc},
vec, rc::Rc, time::SystemTime,
};
use anyhow::Result;
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
use eframe::CreationContext;
use egui::{text::LayoutJob, Color32, ColorImage, TextureHandle};
use lazy_regex::regex_replace_all;
use serde_json::Value;
use lazy_static::lazy_static;
use tokio::task::JoinHandle;
use git_version::git_version;
use version::version;
use crate::ubus::{self, escape_json};
const ERROR_COLOR: Color32 = Color32::from_rgb(180, 20, 20);
const SUCCESS_COLOR: Color32 = Color32::from_rgb(20, 150, 20);
lazy_static! {
pub static ref COPY_ICON: ColorImage = load_image_from_memory(include_bytes!("./copy.png"))
.expect("Failed to load copy icon") as ColorImage;
}
const SOURCE_CODE_URL: &str = "https://example.com";
const VERSION: &str = version!();
const GIT_VERSION: &str = git_version!(args = ["--always", "--dirty=*"]);
pub fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}
pub enum AsyncEvent {
Connect(Result<AsyncSession<AsyncIoTcpStream>>),
Disconnect(Result<()>),
ListObjects(Result<Vec<ubus::Object>>),
Call(Result<Value>)
}
pub struct AppSettings {
address: String,
port: u16,
username: String,
password: String,
show_object_ids: bool,
connect_on_start: bool
}
pub struct App {
settings: AppSettings,
session: Option<AsyncSession<AsyncIoTcpStream>>,
ubus_call_handle: Option<JoinHandle<()>>,
last_ubus_call_at: SystemTime,
selected_object: Option<Rc<ubus::Object>>,
selected_method: Option<String>,
object_filter: String,
objects: Vec<Rc<ubus::Object>>,
payload: String,
response: Option<Result<Value>>,
is_connecting: bool,
is_disconnecting: bool,
settings_open: bool,
tx: Sender<AsyncEvent>,
rx: Receiver<AsyncEvent>,
copy_texture: Option<TextureHandle>
}
impl Default for App {
fn default() -> Self {
let (tx, rx) = std::sync::mpsc::channel();
Self {
settings: AppSettings {
address: "172.24.224.1".into(), //"192.168.1.1".to_owned(),
port: 22,
username: "root".to_owned(),
password: "admin01".to_owned(),
show_object_ids: false,
connect_on_start: true
},
session: None,
ubus_call_handle: None,
last_ubus_call_at: SystemTime::UNIX_EPOCH,
object_filter: "".into(),
selected_object: None,
selected_method: None,
payload: "".into(),
response: None,
objects: vec![],
is_connecting: false,
is_disconnecting: false,
settings_open: false,
tx,
rx,
copy_texture: None
}
}
}
async fn connect<A>(
socket_addr: A,
username: String,
password: String,
) -> Result<AsyncSession<AsyncIoTcpStream>>
where
A: Into<SocketAddr>,
{
let mut session = AsyncSession::<AsyncIoTcpStream>::connect(socket_addr, None).await?;
session.handshake().await?;
session.userauth_password(&username, &password).await?;
return Ok(session);
}
async fn disconnect(session: AsyncSession<AsyncIoTcpStream>) -> Result<()> {
session
.disconnect(
Some(ssh2::DisconnectCode::ByApplication),
"Disconnect",
Some("en"),
)
.await?;
Ok(())
}
fn remove_json_comments(text: &str) -> String {
let text = regex_replace_all!(r#"/\*(.|\n)*?\*/"#, &text, |_, _| ""); // Multi line comments
let text = regex_replace_all!(r#"//.*\n?"#, &text, |_| ""); // Single line comments
let text = regex_replace_all!(r#"(,)(\s*[\]}])"#, &text, |_, _, rest: &str| rest.to_string()); // Trailing commas
text.into()
}
#[derive(Default)]
pub struct App;
fn json_layouter(ui: &egui::Ui, string: &str, wrap_width: f32) -> Arc<egui::Galley> {
let mut layout_job = crate::syntax_highlighting::highlight(ui.ctx(), string, "json");
layout_job.wrap.max_width = wrap_width;
ui.fonts(|f| f.layout_job(layout_job))
}
impl App {
pub fn init(&mut self, _cc: &CreationContext) {
pub fn init(&mut self, cc: &CreationContext) {
if self.settings.connect_on_start {
let username = &self.settings.username;
let password = &self.settings.password;
if username.is_empty() || password.is_empty() {
return;
}
let address = self.settings.address.parse();
if address.is_err() {
return;
}
let address = address.unwrap();
let port = self.settings.port;
let socket_addr = SocketAddrV4::new(address, port);
self.start_connect(socket_addr, username.clone(), password.clone());
}
self.copy_texture = Some(cc.egui_ctx.load_texture(
"clipboard",
COPY_ICON.clone(),
Default::default()
));
}
pub fn get_selected_object(&self) -> Option<&str> {
self.selected_object.as_ref().map(|obj| obj.name.as_str())
}
pub fn get_selected_method(&self) -> Option<&str> {
self.selected_method.as_ref().map(|method| method.as_str())
}
pub fn get_payload(&self) -> serde_json::Result<Value> {
let stripped_payload = remove_json_comments(&self.payload);
serde_json::from_str(&stripped_payload)
}
pub fn is_ubus_call_in_progress(&self) -> bool {
if let Some(handle) = &self.ubus_call_handle {
return !handle.is_finished();
}
return false;
}
fn handle_events(&mut self, _ctx: &egui::Context) {
use AsyncEvent::*;
if let Ok(event) = self.rx.try_recv() {
match event {
Connect(result) => {
self.is_connecting = false;
match result {
Ok(session) => {
self.session = Some(session);
self.start_list_objects()
}
Err(err) => todo!("{}", err),
}
}
Disconnect(result) => {
self.is_disconnecting = false;
if let Err(err) = result {
todo!("{}", err)
}
}
ListObjects(result) => match result {
Ok(objects) => self.objects = objects.into_iter().map(Rc::new).collect(),
Err(err) => todo!("{}", err),
},
Call(result) => {
self.response = Some(result);
self.ubus_call_handle = None;
}
}
}
}
fn start_connect<A>(&mut self, socket_addr: A, username: String, password: String)
where
A: Into<SocketAddr>,
{
if self.session.is_some() {
return;
}
self.is_connecting = true;
let tx = self.tx.clone();
let socket_addr = socket_addr.into();
tokio::spawn(async move {
let result = connect(socket_addr, username, password).await;
tx.send(AsyncEvent::Connect(result))
.expect("Failed to send event");
});
}
fn start_disconnect(&mut self) {
if self.session.is_none() {
return;
}
self.is_disconnecting = true;
let tx = self.tx.clone();
let session = self.session.clone().unwrap();
self.session = None;
tokio::spawn(async move {
let result = disconnect(session).await;
tx.send(AsyncEvent::Disconnect(result))
.expect("Failed to send event");
});
}
fn start_call(&mut self, object: String, method: String, message: Option<Value>) {
if self.session.is_none() {
return;
}
let tx = self.tx.clone();
let session = self.session.clone().unwrap();
let handle = tokio::spawn(async move {
let result = ubus::call(&session, &object, &method, message.as_ref()).await;
tx.send(AsyncEvent::Call(result)).expect("Failed to send event");
});
self.ubus_call_handle = Some(handle);
self.last_ubus_call_at = SystemTime::now()
}
fn start_list_objects(&self) {
if self.session.is_none() {
return;
}
let session = self.session.clone().unwrap();
let tx = self.tx.clone();
tokio::spawn(async move {
let result = ubus::list_verbose(&session, None).await;
tx.send(AsyncEvent::ListObjects(result))
.expect("Failed to send event");
});
}
fn show_left_panel(&mut self, ui: &mut egui::Ui) {
use egui::*;
let mut match_exists = false;
ui.text_edit_singleline(&mut self.object_filter);
ui.add_space(8.0);
ScrollArea::vertical().show(ui, |ui| {
for obj in self.objects.iter() {
let shown_methods;
if obj.name.contains(&self.object_filter) {
shown_methods = obj.methods.iter()
.by_ref()
.collect::<Vec<_>>();
} else {
shown_methods = obj.methods.iter()
.filter(|(name, _)| name.contains(&self.object_filter))
.collect::<Vec<_>>();
}
if shown_methods.len() > 0 {
match_exists = true;
let style = ui.style();
let mut text = LayoutJob::default();
text.append(&obj.name, 0.0, TextFormat {
..TextFormat::default()
});
if self.settings.show_object_ids {
text.append(&format!("@{:x}", obj.id), 10.0, TextFormat {
color: style.noninteractive().fg_stroke.color,
italics: true,
..TextFormat::default()
});
}
CollapsingHeader::new(text).show(ui, |ui| {
for (name, method_params) in &shown_methods {
if ui.selectable_label(false, name).clicked() {
self.selected_object = Some(obj.clone());
self.selected_method = Some(name.clone());
let method_params = method_params.iter().map(|(s, p)| (s.as_str(), *p)).collect::<Vec<_>>();
self.payload = App::create_default_payload(&method_params);
};
}
});
}
}
});
if !match_exists {
ui.label("no matches :(");
}
}
fn show_settings(ui: &mut egui::Ui, settings: &mut AppSettings) {
ui.checkbox(&mut settings.show_object_ids, "Show object IDs");
ui.checkbox(&mut settings.connect_on_start, "Connect on start");
ui.add_space(16.0);
ui.label(format!("Version: {}-{}", VERSION, GIT_VERSION));
ui.label("Author: Rokas Puzonas");
if ui.link("Source code").clicked() {
ui.output_mut(|o| o.open_url(SOURCE_CODE_URL));
}
}
fn show_right_panel(&mut self, ui: &mut egui::Ui) {
use egui::*;
egui::ScrollArea::vertical().show(ui, |ui| {
ui.label("Address:");
ui.text_edit_singleline(&mut self.settings.address);
// TODO: ui.text_edit_singleline(&mut self.port);
ui.label("Username:");
ui.text_edit_singleline(&mut self.settings.username);
ui.label("Password:");
ui.text_edit_singleline(&mut self.settings.password);
ui.add_space(8.0);
if self.is_connecting {
ui.add_enabled(false, Button::new("Connecting..."));
} else if self.session.is_none() {
if ui.button("Connect").clicked() {
let socket_addr = SocketAddrV4::new(self.settings.address.parse().unwrap(), self.settings.port);
self.start_connect(socket_addr, self.settings.username.clone(), self.settings.password.clone());
}
} else {
if ui.button("Disconnect").clicked() {
self.start_disconnect()
}
}
if ui.button("Settings").clicked() {
self.settings_open = !self.settings_open;
}
let window = egui::Window::new("Settings")
.resizable(false)
.collapsible(false)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.open(&mut self.settings_open);
window.show(ui.ctx(), |ui| App::show_settings(ui, &mut self.settings));
ui.add_space(32.0);
ui.label("TODO: Add saving calls");
ui.label("TODO: Add listen logs");
ui.label("TODO: Add monitor logs");
});
}
fn create_default_payload(params: &[(&str, ubus::UbusParamType)]) -> String {
let mut lines = vec![];
for (param_name, param_type) in params {
use ubus::UbusParamType::*;
let param_value = match param_type {
Unknown => "\"<unknown>\"",
Integer => "0",
Boolean => "false",
Table => "{}",
String => "\"\"",
Array => "[]",
Double => "0.00",
};
lines.push(format!("\t\"{}\": {}, // {}", &param_name, &param_value, param_type));
}
return format!("{{\n{}\n}}", lines.join("\n"));
}
fn display_response_textbox(ui: &mut egui::Ui, response: &Result<Value>) {
egui::ScrollArea::vertical().show(ui, |ui| {
let mut text = match response {
Ok(Value::Null) => "Success!".into(),
Ok(response) => serde_json::to_string_pretty(response).unwrap(),
Err(err) => format!("Error: {}", err)
};
let textbox = egui::TextEdit::multiline(&mut text)
.font(egui::TextStyle::Monospace) // for cursor height
.code_editor()
.desired_rows(4)
.lock_focus(true)
.desired_width(f32::INFINITY);
match response {
Ok(Value::Null) => {
ui.add(textbox.text_color(SUCCESS_COLOR));
},
Err(_) => {
ui.add(textbox.text_color(ERROR_COLOR));
},
Ok(_) => {
ui.add(textbox.layouter(&mut json_layouter));
}
};
});
}
fn show_central_panel(&mut self, ui: &mut egui::Ui) {
use egui::*;
let time_since_last_call = self.last_ubus_call_at.elapsed().expect("Failed to get time since last ubus call");
let call_in_progress = self.is_ubus_call_in_progress() && time_since_last_call.as_millis() >= 100;
let call_enabled = self.selected_object.is_some() && self.selected_method.is_some();
ui.horizontal(|ui| {
ui.add_enabled_ui(!call_in_progress, |ui| {
// Object dropdown
{
let object_name = self.selected_object.as_ref().map(|obj| obj.name.as_ref()).unwrap_or("");
let object_combobox = egui::ComboBox::from_id_source("selected_object")
.selected_text(object_name)
.width(200.0)
.show_ui(ui, |ui| {
let mut selection = None;
for object in &self.objects {
ui.selectable_value(&mut selection, Some(object.clone()), &object.name);
}
return selection;
});
match object_combobox.inner {
Some(Some(object)) => {
self.selected_method = None;
self.selected_object = Some(object);
},
_ => {}
};
}
// Method dropdown
{
let selected_method_name = self.selected_method.as_deref().unwrap_or("");
let method_combobox = ui.add_enabled_ui(self.selected_object.is_some(), |ui| {
let mut selection = None;
egui::ComboBox::from_id_source("selected_method")
.selected_text(selected_method_name)
.width(200.0)
.show_ui(ui, |ui| {
if let Some(object) = &self.selected_object {
for method in &object.methods {
let method_name = &method.0;
let mut label_response = ui.selectable_label(selected_method_name == method_name, method_name);
if label_response.clicked() && selected_method_name != method_name {
selection = Some(method_name.clone());
label_response.mark_changed();
}
}
}
});
return selection;
});
match (method_combobox.inner, &self.selected_object) {
(Some(method), Some(object)) => {
let method_params = object.methods.iter()
.find(|(name, _)| name.eq(&method))
.map(|(_, params)| params)
.unwrap()
.iter()
.map(|(param_name, param_type)| (param_name.as_str(), *param_type))
.collect::<Vec<_>>();
self.payload = App::create_default_payload(&method_params);
self.selected_method = Some(method);
},
_ => {}
};
}
if ui.add_enabled(call_enabled, Button::new("call")).clicked() {
let object_name = self.get_selected_object().unwrap().into();
let method_name = self.get_selected_method().unwrap().into();
let payload = self.get_payload().unwrap(); // TODO: handle parsing error
self.start_call(object_name, method_name, Some(payload));
// TODO: Block sending other requests
}
});
let copy_icon = self.copy_texture.as_ref().expect("Copy icon not loaded");
let copy_button = Button::image_and_text(copy_icon.id(), Vec2::new(10.0, 10.0), "copy");
if ui.add_enabled(call_enabled, copy_button).clicked() {
let object_name = self.get_selected_object().unwrap();
let method_name = self.get_selected_method().unwrap();
let payload = self.get_payload().unwrap(); // TODO: handle parsing error
let cmd = format!("ubus call {} {} {}", object_name, method_name, escape_json(&payload));
ui.output_mut(|o| o.copied_text = cmd);
}
if call_in_progress {
ui.spinner();
}
});
ui.separator();
if let Some(response) = &self.response {
egui::TopBottomPanel::bottom("bottom_panel")
.resizable(true)
.show_inside(ui, |ui| {
ui.add_space(10.0);
App::display_response_textbox(ui, response);
});
}
egui::CentralPanel::default()
.show_inside(ui, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
ui.add_enabled(
!call_in_progress,
egui::TextEdit::multiline(&mut self.payload)
.font(egui::TextStyle::Monospace) // for cursor height
.code_editor()
.desired_rows(10)
.lock_focus(true)
.desired_width(f32::INFINITY)
.layouter(&mut json_layouter),
);
});
});
}
}
impl eframe::App for App {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
use egui::*;
self.handle_events(ctx);
egui::CentralPanel::default()
.show(ctx, |ui| {
ui.label("Hello World!");
egui::CentralPanel::default().show(ctx, |ui| {
egui::SidePanel::left("left_panel")
.resizable(true)
.width_range(100.0..=300.0)
.show_inside(ui, |ui| self.show_left_panel(ui));
egui::SidePanel::right("right_panel")
.resizable(true)
.width_range(125.0..=200.0)
.show_inside(ui, |ui| self.show_right_panel(ui));
egui::CentralPanel::default()
.show_inside(ui, |ui| self.show_central_panel(ui));
});
}
}
}

65
src/async_line_reader.rs Normal file
View File

@ -0,0 +1,65 @@
use std::net::TcpStream;
use async_ssh2_lite::AsyncStream;
use smol::{Async, io::AsyncReadExt};
use anyhow::Result;
const INITIAL_CAPACITY: usize = 1024;
pub struct AsyncLineReader {
stream: AsyncStream<Async<TcpStream>>,
line_buffer: Vec<u8>,
buffer_size: usize
}
impl AsyncLineReader {
pub fn new(stream: AsyncStream<Async<TcpStream>>) -> Self {
Self {
stream,
line_buffer: vec![0u8; INITIAL_CAPACITY],
buffer_size: 0
}
}
pub async fn read_line(&mut self) -> Result<Vec<u8>> {
let delim_pos = self.line_buffer[0..self.buffer_size]
.iter()
.position(|c| *c == b'\n');
if let Some(pos) = delim_pos {
let line = self.line_buffer[0..pos].to_vec();
self.line_buffer.copy_within((pos+1)..self.buffer_size, 0);
self.buffer_size -= pos;
self.buffer_size -= 1;
return Ok(line);
}
loop {
// Double line buffer size if at capacity
if self.buffer_size == self.line_buffer.len() {
for _ in 0..=self.line_buffer.len() {
self.line_buffer.push(0u8);
}
}
let n = self.stream.read(&mut self.line_buffer[self.buffer_size..]).await?;
let delim_pos = self.line_buffer[self.buffer_size..(self.buffer_size+n)]
.iter()
.position(|c| *c == b'\n')
.map(|pos| pos + self.buffer_size);
self.buffer_size += n;
if let Some(pos) = delim_pos {
let line = self.line_buffer[0..pos].to_vec();
self.line_buffer.copy_within((pos+1)..self.buffer_size, 0);
self.buffer_size -= pos;
self.buffer_size -= 1;
return Ok(line);
}
}
}
}

BIN
src/copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
src/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
src/icon.xcf Normal file

Binary file not shown.

View File

@ -1,64 +1,46 @@
#[cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use std::{net::ToSocketAddrs, thread};
use anyhow::Result;
use smol::{channel, block_on};
use async_ssh2_lite::{AsyncSession};
use app::App;
use crate::ubus::{Ubus, UbusEvent};
use eframe::IconData;
use egui::Vec2;
use tokio::runtime::Runtime;
mod app;
mod ubus;
mod async_line_reader;
mod syntax_highlighting;
#[tokio::main]
async fn main() -> Result<()> {
let address = "172.24.224.1:22";
let username = "root";
let password = "admin01";
// TODO: Save config to file
// TODO: call history
let mut session = AsyncSession::<async_ssh2_lite::AsyncIoTcpStream>::connect(
address.to_socket_addrs()?.next().unwrap(),
None,
).await?;
pub fn load_icon_from_memory(image_data: &[u8]) -> Result<IconData, image::ImageError> {
let image = image::load_from_memory(image_data)?;
Ok(IconData {
rgba: image.to_rgba8().to_vec(),
width: image.width(),
height: image.height(),
})
}
session.handshake().await?;
session.userauth_password(username, password).await?;
fn main() {
let rt = Runtime::new().expect("Unable to create Runtime");
let ubus = Ubus::new(session);
let (tx, rx) = channel::unbounded::<UbusEvent>();
let listener = {
let tx = tx.clone();
tokio::spawn(async move {
println!("before listen");
if let Err(err) = ubus.listen(&[], tx).await {
dbg!(err);
};
println!("after listen");
})
};
let _enter = rt.enter();
loop {
let e = rx.recv().await?;
dbg!(e);
}
/*
let mut native_options = eframe::NativeOptions::default();
native_options.decorated = true;
native_options.resizable = true;
let mut app = App::default();
let mut options = eframe::NativeOptions::default();
options.min_window_size = Some(Vec2::new(920.0, 500.0));
let icon_data = load_icon_from_memory(include_bytes!("./icon.png")).expect("Failed to load icon data");
options.icon_data = Some(icon_data);
options.vsync = true;
eframe::run_native(
"ubusman",
native_options,
options,
Box::new(move |cc| {
app.init(cc);
Box::new(app)
})
)
*/
Ok(())
).expect("Unable to create window");
}

107
src/syntax_highlighting.rs Normal file
View File

@ -0,0 +1,107 @@
use egui::text::LayoutJob;
const THEME: &str = "base16-mocha.dark";
/// Memoized Code highlighting
pub fn highlight(ctx: &egui::Context, code: &str, language: &str) -> LayoutJob {
impl egui::util::cache::ComputerMut<(&str, &str), LayoutJob> for Highlighter {
fn compute(&mut self, (code, lang): (&str, &str)) -> LayoutJob {
self.highlight(code, lang)
}
}
type HighlightCache = egui::util::cache::FrameCache<LayoutJob, Highlighter>;
ctx.memory_mut(|mem| {
mem.caches
.cache::<HighlightCache>()
.get((code, language))
})
}
// ----------------------------------------------------------------------------
struct Highlighter {
ps: syntect::parsing::SyntaxSet,
ts: syntect::highlighting::ThemeSet,
}
impl Default for Highlighter {
fn default() -> Self {
Self {
ps: syntect::parsing::SyntaxSet::load_defaults_newlines(),
ts: syntect::highlighting::ThemeSet::load_defaults(),
}
}
}
impl Highlighter {
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn highlight(&self, code: &str, lang: &str) -> LayoutJob {
self.highlight_impl(code, lang).unwrap_or_else(|| {
// Fallback:
LayoutJob::simple(
code.into(),
egui::FontId::monospace(12.0),
egui::Color32::DARK_GRAY,
f32::INFINITY,
)
})
}
fn highlight_impl(&self, text: &str, language: &str) -> Option<LayoutJob> {
use syntect::easy::HighlightLines;
use syntect::highlighting::FontStyle;
use syntect::util::LinesWithEndings;
let syntax = self
.ps
.find_syntax_by_name(language)
.or_else(|| self.ps.find_syntax_by_extension(language))?;
let mut h = HighlightLines::new(syntax, &self.ts.themes[THEME]);
use egui::text::{LayoutSection, TextFormat};
let mut job = LayoutJob {
text: text.into(),
..Default::default()
};
for line in LinesWithEndings::from(text) {
for (style, range) in h.highlight_line(line, &self.ps).ok()? {
let fg = style.foreground;
let text_color = egui::Color32::from_rgb(fg.r, fg.g, fg.b);
let italics = style.font_style.contains(FontStyle::ITALIC);
let underline = style.font_style.contains(FontStyle::ITALIC);
let underline = if underline {
egui::Stroke::new(1.0, text_color)
} else {
egui::Stroke::NONE
};
job.sections.push(LayoutSection {
leading_space: 0.0,
byte_range: as_byte_range(text, range),
format: TextFormat {
font_id: egui::FontId::monospace(12.0),
color: text_color,
italics,
underline,
..Default::default()
},
});
}
}
Some(job)
}
}
fn as_byte_range(whole: &str, range: &str) -> std::ops::Range<usize> {
let whole_start = whole.as_ptr() as usize;
let range_start = range.as_ptr() as usize;
assert!(whole_start <= range_start);
assert!(range_start + range.len() <= whole_start + whole.len());
let offset = range_start - whole_start;
offset..(offset + range.len())
}

View File

@ -1,19 +1,21 @@
use std::{io::Read, borrow::Cow, net::TcpStream};
use std::{borrow::Cow, str::FromStr, fmt::Display};
use async_ssh2_lite::AsyncSession;
use lazy_regex::regex_captures;
use serde_json::{Value, json};
use shell_escape::unix::escape;
use anyhow::{anyhow, bail, Result};
use async_ssh2_lite::{AsyncIoTcpStream, AsyncSession};
use hex::FromHex;
use anyhow::{Result, bail, anyhow};
use smol::{Async, io::AsyncReadExt, channel::Sender};
use lazy_regex::regex_captures;
use serde_json::Value;
use shell_escape::unix::escape;
use smol::{channel::Sender, io::AsyncReadExt};
use thiserror::Error;
pub struct Ubus {
session: AsyncSession<Async<TcpStream>>
}
use crate::async_line_reader::AsyncLineReader;
#[derive(Debug)]
type Session = AsyncSession<AsyncIoTcpStream>;
// TODO: Add tests
#[derive(Debug, Clone, Copy)]
pub enum UbusParamType {
Unknown,
Integer,
@ -21,7 +23,7 @@ pub enum UbusParamType {
Table,
String,
Array,
Double
Double,
}
#[derive(Error, Debug)]
@ -54,17 +56,113 @@ pub enum UbusError {
SystemError,
}
#[derive(Debug)]
pub struct UbusObject {
pub type Method = (String, Vec<(String, UbusParamType)>);
#[derive(Debug, Clone)]
pub struct Object {
pub name: String,
pub id: u32,
pub methods: Vec<(String, Vec<(String, UbusParamType)>)>
pub methods: Vec<Method>,
}
impl PartialEq for Object {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.id == other.id
}
}
#[derive(Debug)]
pub struct UbusEvent {
pub struct ListenEvent {
pub path: String,
pub value: Value
pub value: Value,
}
#[derive(Debug, Clone, Copy)]
pub enum MonitorEventType {
Hello,
Status,
Data,
Ping,
Lookup,
Invoke,
AddObject,
RemoveObject,
Subscribe,
Unsubscribe,
Notify,
}
impl ToString for MonitorEventType {
fn to_string(&self) -> String {
use MonitorEventType::*;
match self {
Hello => "hello",
Status => "status",
Data => "data",
Ping => "ping",
Lookup => "lookup",
Invoke => "invoke",
AddObject => "add_object",
RemoveObject => "remove_object",
Subscribe => "subscribe",
Unsubscribe => "unsubscribe",
Notify => "notify",
}
.into()
}
}
impl FromStr for MonitorEventType {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
use MonitorEventType::*;
match s {
"hello" => Ok(Hello),
"status" => Ok(Status),
"data" => Ok(Data),
"ping" => Ok(Ping),
"lookup" => Ok(Lookup),
"invoke" => Ok(Invoke),
"add_object" => Ok(AddObject),
"remove_object" => Ok(RemoveObject),
"subscribe" => Ok(Subscribe),
"unsubscribe" => Ok(Unsubscribe),
"notify" => Ok(Notify),
_ => Err(anyhow!("Unknown event type '{}'", s)),
}
}
}
impl Display for UbusParamType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use UbusParamType::*;
let text = match self {
String => "String",
Boolean => "Boolean",
Integer => "Integer",
Double => "Double",
Table => "Table",
Array => "Array",
Unknown => "(unknown)"
};
f.write_str(text)
}
}
#[derive(Debug)]
pub struct MonitorEvent {
direction: MonitorDir,
client: u32,
peer: u32,
kind: MonitorEventType,
data: Value, // TODO: Figure out the possible values for every `MonitorEventType`, to make this more safe.
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MonitorDir {
Rx,
Tx,
}
fn parse_parameter_type(param: &str) -> Option<UbusParamType> {
@ -77,7 +175,7 @@ fn parse_parameter_type(param: &str) -> Option<UbusParamType> {
"Table" => Some(Table),
"Array" => Some(Array),
"(unknown)" => Some(Unknown),
_ => None
_ => None,
}
}
@ -95,11 +193,11 @@ fn parse_error_code(code: i32) -> Option<UbusError> {
7 => Some(Timeout),
8 => Some(NotSupported),
9 => Some(UnknownError),
10 => Some(ConnectionFailed),
10 | -1 => Some(ConnectionFailed),
11 => Some(NoMemory),
12 => Some(ParseError),
13 => Some(SystemError),
_ => Some(UnknownError)
_ => Some(UnknownError),
}
}
@ -113,177 +211,228 @@ fn parse_hex_id(id: &str) -> Result<u32> {
return Ok(byte1 + byte2 + byte3 + byte4);
}
fn escape_json(json: &Value) -> String {
pub fn escape_json(json: &Value) -> String {
escape(Cow::from(json.to_string())).into()
}
impl Ubus {
pub fn new(session: AsyncSession<Async<TcpStream>>) -> Ubus {
Ubus {
session
}
async fn exec_cmd(session: &Session, cmd: &str) -> Result<String> {
let mut channel = session.channel_session().await?;
channel.exec(cmd).await?;
channel.send_eof().await?;
channel.wait_eof().await?;
channel.close().await?;
channel.wait_close().await?;
if let Some(err) = parse_error_code(channel.exit_status()?) {
return Err(err.into());
}
async fn exec_cmd(&self, cmd: &str) -> Result<String> {
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 string_buffer = String::new();
channel.read_to_string(&mut string_buffer).await?;
Ok(string_buffer)
}
let mut output = String::new();
channel.read_to_string(&mut output).await?;
Ok(output)
}
pub async fn list(session: &Session, path: Option<&str>) -> Result<Vec<String>> {
let output = match path {
Some(path) => exec_cmd(session, &format!("ubus -S list {}", path)).await?,
None => exec_cmd(session, "ubus -S list").await?,
};
Ok(output.lines().map(ToOwned::to_owned).collect::<Vec<_>>())
}
pub async fn list(self, path: Option<&str>) -> Result<Vec<String>> {
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::<Vec<_>>())
}
pub async fn list_verbose(session: &Session, path: Option<&str>) -> Result<Vec<Object>> {
let output = match path {
Some(path) => exec_cmd(session, &format!("ubus -v list {}", path)).await?,
None => exec_cmd(session, "ubus -v list").await?,
};
pub async fn list_verbose(self, path: Option<&str>) -> Result<Vec<UbusObject>> {
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 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<Value> {
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::<Value>(&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(())
}
fn parse_event(bytes: &[u8]) -> Result<UbusEvent> {
let event_value: Value = serde_json::from_slice(bytes)?;
let event_map = event_value.as_object()
.ok_or(anyhow!("Expected event to be an object"))?;
if event_map.keys().len() != 1 {
bail!("Expected event object to only contain one key");
}
let path = event_map.keys().next().unwrap().clone();
let value = event_map.get(&path).unwrap().clone();
Ok(UbusEvent { path, value })
}
pub async fn listen(self, paths: &[&str], sender: Sender<UbusEvent>) -> Result<()> {
let cmd = format!("ubus listen {}", paths.join(" "));
let mut channel = self.session.channel_session().await?;
channel.exec(&cmd).await?;
// TODO: Handle error? 'channel.exit_status()', idk if neededdi
let mut line_buffer = vec![0u8; 1024];
let mut buffer_size = 0usize;
loop {
let n = channel.read(&mut line_buffer[buffer_size..]).await?;
let delim_pos = line_buffer[buffer_size..(buffer_size+n)]
.iter()
.position(|c| *c == b'\n')
.map(|pos| pos + buffer_size);
buffer_size += n;
if let Some(pos) = delim_pos {
let event = Ubus::parse_event(&line_buffer[0..pos])?;
sender.send(event).await?;
line_buffer.copy_within((pos+1)..buffer_size, 0);
buffer_size -= pos;
buffer_size -= 1;
let mut objects = vec![];
for line in output.lines() {
if let Some((_, name, id)) = regex_captures!(r"^'([\w.-]+)' @([0-9a-zA-Z]{8})$", line) {
if cur_name.is_some() && cur_id.is_some() {
objects.push(Object {
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
))?;
// Double line buffer size if at capacity
if buffer_size == line_buffer.len() {
for _ in 0..=line_buffer.len() {
line_buffer.push(0u8);
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 subscribe(self, paths: &[&str]) -> Result<()> {
if paths.len() < 1 {
bail!("At least 1 object is required")
}
let cmd = format!("ubus subscribe {}", paths.join(" "));
let mut channel = self.session.channel_session().await?;
channel.exec(&cmd).await?;
pub async fn call(
session: &Session,
path: &str,
method: &str,
message: Option<&Value>,
) -> Result<Value> {
let cmd = match message {
Some(msg) => format!("ubus -S call {} {} {}", path, method, escape_json(msg)),
None => format!("ubus -S call {} {}", path, method),
};
// TODO: Haven't figured out how to test subscribe event using default objects on ubus.
todo!();
// TODO: handle cases where output is empty? ("")
let output = exec_cmd(session, &cmd).await?;
if output.is_empty() {
return Ok(Value::Null);
}
let value = serde_json::from_str::<Value>(&output)?;
Ok(value)
}
pub async fn send(session: &Session, event_type: &str, message: Option<&Value>) -> Result<()> {
let cmd = match message {
Some(msg) => format!("ubus -S send {} {}", event_type, escape_json(msg)),
None => format!("ubus -S send {}", event_type),
};
exec_cmd(session, &cmd).await?;
Ok(())
}
pub async fn wait_for(session: &Session, objects: &[&str]) -> Result<()> {
if objects.len() < 1 {
bail!("At least 1 object is required")
}
let cmd = format!("ubus -S wait_for {}", objects.join(" "));
let mut channel = session.channel_session().await?;
channel.exec(&cmd).await?;
channel.close().await?;
Ok(())
}
fn parse_listen_event(bytes: &[u8]) -> Result<ListenEvent> {
let event_value: Value = serde_json::from_slice(bytes)?;
let event_map = event_value
.as_object()
.ok_or(anyhow!("Expected event to be an object"))?;
if event_map.keys().len() != 1 {
bail!("Expected event object to only contain one key");
}
let path = event_map.keys().next().unwrap().clone();
let value = event_map.get(&path).unwrap().clone();
Ok(ListenEvent { path, value })
}
pub async fn listen(session: &Session, paths: &[&str], sender: Sender<ListenEvent>) -> Result<()> {
let cmd = format!("ubus -S listen {}", paths.join(" "));
let mut channel = session.channel_session().await?;
channel.exec(&cmd).await?;
// TODO: Handle error? 'channel.exit_status()', idk if needed
let mut line_reader = AsyncLineReader::new(channel.stream(0));
loop {
let line = line_reader.read_line().await?;
let event = parse_listen_event(&line)?;
sender.send(event).await?;
}
}
pub async fn subscribe(session: &Session, paths: &[&str]) -> Result<()> {
if paths.len() < 1 {
bail!("At least 1 object is required")
}
let cmd = format!("ubus -S subscribe {}", paths.join(" "));
let mut channel = session.channel_session().await?;
channel.exec(&cmd).await?;
// TODO: Haven't figured out how to test subscribe event using default objects on ubus.
todo!();
}
fn parse_monitor_event(bytes: &[u8]) -> Result<MonitorEvent> {
let line = bytes
.iter()
.map(|c| char::from_u32(*c as u32).unwrap())
.collect::<String>();
let (_, direction_str, client_str, peer_str, kind_str, data_str) = regex_captures!(
r"^([<\->]{2}) ([0-9a-zA-Z]{8}) #([0-9a-zA-Z]{8})\s+([a-z_]+): (\{.*\})$",
&line
)
.ok_or(anyhow!("Unknown pattern of monitor message '{}'", line))?;
let direction = match direction_str {
"->" => MonitorDir::Tx,
"<-" => MonitorDir::Rx,
_ => bail!("Unknown monitor message direction '{}'", direction_str),
};
let client = parse_hex_id(client_str)?;
let peer = parse_hex_id(peer_str)?;
let kind = MonitorEventType::from_str(kind_str)?;
let data = serde_json::from_str(data_str)?;
Ok(MonitorEvent {
direction,
client,
peer,
kind,
data,
})
}
pub async fn monitor(
session: &Session,
dir: Option<MonitorDir>,
filter: &[MonitorEventType],
sender: Sender<MonitorEvent>,
) -> Result<()> {
let mut cmd = vec!["ubus -S".into()];
if let Some(dir) = dir {
if dir == MonitorDir::Rx {
cmd.push("-M r".into());
} else if dir == MonitorDir::Tx {
cmd.push("-M t".into());
}
}
cmd.extend(filter.iter().map(|e| format!("-m {}", e.to_string())));
cmd.push("monitor".into());
let mut channel = session.channel_session().await?;
println!("{}", cmd.join(" "));
channel.exec(&cmd.join(" ")).await?;
// TODO: Handle error? 'channel.exit_status()', idk if needed
let mut line_reader = AsyncLineReader::new(channel.stream(0));
loop {
let line = line_reader.read_line().await?;
let event = parse_monitor_event(&line)?;
sender.send(event).await?;
}
}