moga + fmt/clippy

This commit is contained in:
lda
2025-08-25 03:02:20 +07:00 Unverified
parent ecbb1c653c
commit 0c10cc734a
13 changed files with 223 additions and 66 deletions
+9 -7
View File
@@ -12,19 +12,21 @@ use std::{net::IpAddr, str::FromStr};
use macaddr::MacAddr;
use serde::{Deserialize, Serialize, Serializer, de};
use serde_with::skip_serializing_none ;
use serde_with::skip_serializing_none;
use strum::{Display, EnumString};
use crate::arpparse::error::IPNeighParseError;
mod error;
mod r#impl; // custom (de)serialization impls
/// ip neigh has some cool shit.
/// IP
/// dev DEV | None
/// lladdr MAC | None
/// status { permanent | noarp | stale | reachable | none | incomplete | delay | probe | failed } (ip neigh help)
/// so you can see its damn good
///
/// IP
/// dev DEV | None
/// lladdr MAC | None
/// status { permanent | noarp | stale | reachable | none | incomplete | delay | probe | failed } (ip neigh help)
///
/// so you can see its damn good
#[skip_serializing_none]
#[derive(Debug, PartialEq, Eq, Clone, Hash, serde::Serialize)]
pub struct IpNeighLine {
@@ -43,7 +45,7 @@ pub fn ser_opm<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S:
}
/// deserialize an [`Option<MacAddr>`]
pub fn des_opm<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
pub fn _des_opm<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
where
D: serde::Deserializer<'de>,
{
+1 -1
View File
@@ -27,4 +27,4 @@ impl From<strum::ParseError> for IPNeighParseError {
fn from(value: strum::ParseError) -> Self {
Self::StateParseError(value)
}
}
}
+17 -7
View File
@@ -7,15 +7,15 @@ use axum::{
};
use tokio::net::TcpListener;
mod arpparse;
mod route;
mod r#static;
mod utils;
mod route;
use crate::{
route::DeviceQuery,
utils::{ping::ping_ip, query::get_macs_2_1, wake::wake},
};
use r#static as st;
use std::io;
const MACHINE_NAME: &str = "lda.lan";
@@ -49,7 +49,11 @@ async fn wake_handler(
Query(DeviceQuery { name, .. }): Query<DeviceQuery>,
) -> axum::response::Result<impl IntoResponse> {
match wake(name.as_deref().unwrap_or(MACHINE_NAME)).await {
Ok(x) if x > 0 => Ok((StatusCode::ACCEPTED, format!("{x} packets sent!"))),
Ok(0) => Err((StatusCode::NOT_FOUND, "No packets sent!").into()),
Ok(x) => Ok((
StatusCode::ACCEPTED,
format!("{x} packet{s} sent!", s = if x > 1 { "s" } else { "" }),
)),
_ => Err((StatusCode::GATEWAY_TIMEOUT, "Wake failed").into()),
}
}
@@ -95,17 +99,17 @@ pub async fn status_build(machine_name: &str) -> String {
}
#[cfg(target_os = "linux")]
#[tokio::main]
async fn main() -> color_eyre::Result<()> {
use crate::route::{api_status, get_status_2, home_2, home_2_route};
async fn entry() -> io::Result<()> {
use crate::route::{api_status, devs_router, get_status_2, home_2, home_2_route};
color_eyre::install()?;
let app = Router::new()
.route("/home", get(home))
.route("/", get(home_2))
.merge(home_2_route())
.route("/wake", post(wake_handler))
.route("/status", get(get_status_2))
.merge(api_status());
.merge(api_status())
.route("/api/devs", get(devs_router));
let port = TcpListener::bind("0.0.0.0:12012").await?;
axum::serve(port, app.into_make_service()).await?;
@@ -124,3 +128,9 @@ fn main() -> color_eyre::Result<()> {
"OS not supported! run this on your ahh router!"
))
}
#[cfg(target_os = "linux")]
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
Ok(entry()?)
}
+75 -11
View File
@@ -1,3 +1,4 @@
use std::collections::HashSet;
use std::net::IpAddr;
use axum::{
@@ -9,17 +10,20 @@ use axum::{
};
use axum_extra::extract::Query;
use macaddr::MacAddr;
use serde::ser::Serializer;
use serde_with::skip_serializing_none;
// use serde_with::skip_serializing_none;
// use serde::{Deserialize, de};
// use serde_with::{OneOrMany, serde_as};
use crate::{
MACHINE_NAME,
arpparse::{self, NUDState, des_opm},
arpparse::{self, NUDState},
st, status_build,
utils::{de_many, query::get_macs},
utils::{
de_many,
query::{
dev::{self, has_dev},
get_macs,
},
},
};
pub async fn home_2() -> Html<&'static str> {
@@ -58,8 +62,8 @@ pub struct DeviceQuery {
// Accept single or many; ignore blanks
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
ip: Vec<IpAddr>,
#[serde(default, deserialize_with = "des_opm")]
mac: Option<MacAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
mac: Vec<MacAddr>,
/// optional interface filter (e.g., br-lan)
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub dev: Vec<String>,
@@ -87,6 +91,19 @@ pub struct Filters {
dev: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
nud: Vec<NUDState>,
#[serde(
skip_serializing_if = "Vec::is_empty",
serialize_with = "serialize_macs"
)]
mac: Vec<MacAddr>,
}
fn serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let strings: Vec<String> = macs.iter().map(|m| m.to_string()).collect();
serde::Serialize::serialize(&strings, serializer)
}
#[skip_serializing_none]
#[derive(Debug, serde::Serialize)]
@@ -98,7 +115,12 @@ pub struct StatusError {
pub async fn get_status_json(
// p: Option<Path<NamePath>>,
Query(DeviceQuery {
name, ip, dev, nud, ..
name,
ip,
dev,
nud,
mac,
..
}): Query<DeviceQuery>,
) -> impl IntoResponse {
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> {
@@ -122,7 +144,12 @@ pub async fn get_status_json(
let dev_opts: Vec<Option<String>> = to_opts(&dev);
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
let filters = Filters { ip, dev, nud };
let filters = Filters {
ip,
dev,
nud,
mac: mac.clone(),
};
// Run combinations of dev/nud and merge results
let mut tasks = Vec::new();
@@ -141,7 +168,12 @@ pub async fn get_status_json(
.await
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
{
Ok(table) => {
Ok(mut table) => {
// Optional MAC post-filtering if provided
if !mac.is_empty() {
let wanted: HashSet<MacAddr> = mac.into_iter().collect();
table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false));
}
// let canonical = format!("/api/status?name={name}");
(
StatusCode::OK,
@@ -175,6 +207,7 @@ pub fn api_status() -> Router {
Router::new()
.route("/api/status/{name}", get(status_redirect)) // api/status should be like the entire ip neigh br lan like idk like
.route("/api/status", get(get_status_json))
.route("/api/smart/{q}", get(status_smart_redirect))
}
pub async fn get_status_2(q: Query<DeviceQuery>) -> Html<String> {
@@ -186,3 +219,34 @@ pub async fn get_status_2(q: Query<DeviceQuery>) -> Html<String> {
};
Html(status_build(&name).await)
}
// Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly
pub async fn status_smart_redirect(Path(q): Path<String>) -> Redirect {
let s = q.trim();
// 1) IP
if let Ok(ip) = s.parse::<IpAddr>() {
return Redirect::to(&format!("/api/status?ip={ip}"));
}
// 2) MAC
if let Ok(mac) = s.parse::<MacAddr>() {
return Redirect::to(&format!("/api/status?mac={mac}"));
}
// 3) NUD state (reachable, stale, ...)
if let Ok(state) = s.parse::<NUDState>() {
return Redirect::to(&format!("/api/status?nud={state}"));
}
// 4) Known device? prefer dev first
if has_dev(s) {
return Redirect::to(&format!("/api/status?dev={}", urlencoding::encode(s)));
}
// 5) Try DNS: if it resolves, treat as name
if tokio::net::lookup_host((s, 0)).await.is_ok() {
return Redirect::to(&format!("/api/status?name={}", urlencoding::encode(s)));
}
// Default: name last
Redirect::to(&format!("/api/status?name={}", urlencoding::encode(s)))
}
pub async fn devs_router() -> Json<Vec<String>> {
dev::devs_sorted().into()
}
+1 -1
View File
@@ -1,3 +1,3 @@
pub const HOME_2: &str = include_str!("../static/home_2");
pub const HOME_2_CSS: &str = include_str!("../static/assets/home_2.css");
pub const HOME_2_JS: &str = include_str!("../static/assets/home_2.js");
pub const HOME_2_JS: &str = include_str!("../static/assets/home_2.js");
+12 -10
View File
@@ -1,16 +1,15 @@
pub const LDA_MACS: [[u8; 6]; 2] = [
// ether
[0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
// wifi
[0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
];
// pub const LDA_MACS: [[u8; 6]; 2] = [
// // ether
// [0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
// // wifi
// [0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
// ];
/// is it time to lookup host lda.lan for this...
pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub mod wake;
use std::{net::IpAddr, sync::LazyLock};
use std::net::IpAddr;
use macaddr::MacAddr;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
@@ -21,7 +20,10 @@ pub mod ping;
pub async fn get_ips(machine_name: &str) -> error::Result<Vec<IpAddr>> {
let it = tokio::net::lookup_host((machine_name, 0))
.await
.map_err(|e| error::Error::DnsResolve { name: machine_name.to_string(), source: e })?;
.map_err(|e| error::Error::DnsResolve {
name: machine_name.to_string(),
source: e,
})?;
Ok(it.map(|c| c.ip()).collect())
}
+1 -1
View File
@@ -7,4 +7,4 @@ pub(crate) async fn exec_command<S: AsRef<std::ffi::OsStr>>(
let mut u = tokio::process::Command::new(cmd);
u.args(args);
u.output().await
}
}
+17 -9
View File
@@ -20,13 +20,19 @@ pub enum Error {
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::DnsResolve { name, source } => write!(f, "DNS resolve failed for {name}: {source}"),
Error::CommandFailed { cmd, args, status, stderr } => {
let code = status.map(|c| c.to_string()).unwrap_or_else(|| "signal".into());
write!(
f,
"{cmd} {args:?} failed (status: {code}): {stderr}",
)
Error::DnsResolve { name, source } => {
write!(f, "DNS resolve failed for {name}: {source}")
}
Error::CommandFailed {
cmd,
args,
status,
stderr,
} => {
let code = status
.map(|c| c.to_string())
.unwrap_or_else(|| "signal".into());
write!(f, "{cmd} {args:?} failed (status: {code}): {stderr}",)
}
Error::Io(e) => write!(f, "IO error: {e}"),
}
@@ -44,5 +50,7 @@ impl std::error::Error for Error {
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self { Error::Io(e) }
}
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
+5 -2
View File
@@ -2,7 +2,10 @@
use std::time::Duration;
use tokio::{net::{TcpStream, ToSocketAddrs}, time::timeout};
use tokio::{
net::{TcpStream, ToSocketAddrs},
time::timeout,
};
pub async fn ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
timeout(Duration::from_secs(1), TcpStream::connect(addr))
@@ -11,4 +14,4 @@ pub async fn ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
}
pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
todo!("use icmp")
}
}
+51 -10
View File
@@ -5,7 +5,11 @@ use macaddr::MacAddr;
use crate::{
arpparse::{self, IpNeighLine, NUDState},
utils::{cmd::exec_command, error::{self, Error, Result}, get_ips},
utils::{
cmd::exec_command,
error::{self, Error, Result},
get_ips,
},
};
pub async fn get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
@@ -76,13 +80,8 @@ pub async fn get_macs(
state: Option<NUDState>,
) -> Result<Vec<IpNeighLine>> {
// Collect IPs early (before any await) to avoid holding generics across await points
let ip_list: Option<Vec<IpAddr>> = ips.map(|slice| {
slice
.iter()
.copied()
.map(|ip| ip.to_canonical())
.collect()
});
let ip_list: Option<Vec<IpAddr>> =
ips.map(|slice| slice.iter().copied().map(|ip| ip.to_canonical()).collect());
// Resolve by machine name if no IPs provided but we have a name
let ip_list = match (ip_list, machine_name) {
@@ -128,15 +127,57 @@ pub async fn get_macs(
} else {
parsed.collect()
};
Ok::<Vec<IpNeighLine>, error::Error>(rows)
Ok::<Vec<IpNeighLine>, error::Error>(rows)
};
// If we have specific IPs, query each; otherwise query the whole table once
if !ip_list.is_empty() {
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip)));
let res = futures::future::try_join_all(futures).await?;
let res = futures::future::try_join_all(futures).await?;
Ok(res.into_iter().flatten().collect())
} else {
run_one(None).await
}
}
pub mod dev {
use std::{collections::HashSet, sync::LazyLock};
pub fn get_dev() -> HashSet<String> {
// Prefer /sys/class/net, fallback to /proc/net/dev; filter out loopback
let mut devs: HashSet<String> = HashSet::new();
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
for e in rd.flatten() {
if let Ok(name) = e.file_name().into_string()
&& name != "lo"
&& !name.is_empty()
{
devs.insert(name);
}
}
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
for line in txt.lines().skip(2) {
// skip headers
if let Some((name, _rest)) = line.split_once(':') {
let n = name.trim().to_string();
if n != "lo" && !n.is_empty() {
devs.insert(n);
}
}
}
}
devs
}
pub static DEVS: LazyLock<HashSet<String>> = LazyLock::new(get_dev);
pub fn devs_sorted() -> Vec<String> {
let mut v: Vec<String> = DEVS.iter().cloned().collect();
v.sort();
v
}
pub fn has_dev(name: &str) -> bool {
DEVS.contains(name)
}
}
+4 -4
View File
@@ -2,13 +2,13 @@ use std::{io, net::IpAddr};
use tokio::net::UdpSocket;
use crate::utils::{query::get_macs_2_mac, LDA_MACS_2};
use crate::utils::query::get_macs_2_mac;
pub async fn wake(machine_name: &str) -> io::Result<u32> {
let suh = UdpSocket::bind("0.0.0.0:0").await?;
suh.set_broadcast(true)?;
let mut macs = get_macs_2_mac(machine_name).await.unwrap_or_default();
macs.extend(*LDA_MACS_2);
let /* mut */ macs = get_macs_2_mac(machine_name).await.unwrap_or_default();
// macs.extend(*LDA_MACS_2);
let mut sent_ok = 0;
for mac in macs {
let mb = mac.as_bytes();
@@ -29,4 +29,4 @@ pub async fn wake(machine_name: &str) -> io::Result<u32> {
}
}
Ok(sent_ok)
}
}