chatgpt lowkey goated
This commit is contained in:
+1
-1
@@ -114,7 +114,7 @@ impl NUDState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// dumb UI label
|
/// dumb UI label
|
||||||
pub fn dumber_state(&self) -> &'static str {
|
pub fn _dumber_state(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
NUDState::Permanent | NUDState::Reachable => "online",
|
NUDState::Permanent | NUDState::Reachable => "online",
|
||||||
NUDState::Stale => "maybe online",
|
NUDState::Stale => "maybe online",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use std::{fs::read_to_string, io, net::IpAddr};
|
||||||
|
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::Serializer;
|
||||||
|
|
||||||
|
/// A single line from /tmp/dhcp.leases
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct DhcpLeaseLine {
|
||||||
|
/// Epoch seconds when the lease expires
|
||||||
|
pub expires_epoch: u64,
|
||||||
|
pub ip: IpAddr,
|
||||||
|
#[serde(serialize_with = "ser_mac")]
|
||||||
|
pub mac: MacAddr,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ser_mac<S: Serializer>(m: &MacAddr, s: S) -> Result<S::Ok, S::Error> {
|
||||||
|
s.serialize_str(&m.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse one line of /tmp/dhcp.leases
|
||||||
|
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLeaseLine> {
|
||||||
|
let mut c = line.split_whitespace();
|
||||||
|
let expires_epoch: u64 = c.next()?.parse().ok()?;
|
||||||
|
let mac = c.next()?.parse().ok()?;
|
||||||
|
let ip = c.next()?.parse().ok()?;
|
||||||
|
let name = c.next().filter(|c| *c != "*").map(str::to_string);
|
||||||
|
// ignore any remaining columns (e.g., client-id)
|
||||||
|
Some(DhcpLeaseLine {
|
||||||
|
expires_epoch,
|
||||||
|
ip,
|
||||||
|
mac,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read all leases from /tmp/dhcp.leases
|
||||||
|
pub fn read_dhcp_leases() -> io::Result<Vec<DhcpLeaseLine>> {
|
||||||
|
let file = read_to_string("/tmp/dhcp.leases")?;
|
||||||
|
Ok(file.lines().flat_map(parse_dhcp_lease_line).collect())
|
||||||
|
}
|
||||||
+6
-93
@@ -1,115 +1,29 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::Query,
|
|
||||||
http::StatusCode,
|
|
||||||
response::{Html, IntoResponse},
|
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
mod arpparse;
|
mod arpparse;
|
||||||
|
mod dhcpparse;
|
||||||
mod route;
|
mod route;
|
||||||
mod r#static;
|
pub mod r#static;
|
||||||
mod utils;
|
mod utils;
|
||||||
use crate::{
|
|
||||||
route::DeviceQuery,
|
|
||||||
utils::{ping::ping_ip, query::get_macs_2_1, wake::wake},
|
|
||||||
};
|
|
||||||
use r#static as st;
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
const MACHINE_NAME: &str = "lda.lan";
|
const MACHINE_NAME: &str = "lda.lan";
|
||||||
|
|
||||||
async fn home() -> Html<String> {
|
|
||||||
Html(format!(
|
|
||||||
r#"
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<p><a href="/home_2">Alternate UI</a></p>
|
|
||||||
<p>the machine is {}! <a href="/status">Status</a></p>
|
|
||||||
<form method="POST" action="/wake">
|
|
||||||
<button type="submit">Wake LDA</button>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"#,
|
|
||||||
if ping_ip((MACHINE_NAME, 22)).await {
|
|
||||||
"on"
|
|
||||||
} else {
|
|
||||||
"off"
|
|
||||||
} // match get_ips(MACHINE_NAME).await {
|
|
||||||
// Ok(ips) => {
|
|
||||||
// // let addrs: Vec<SocketAddr> = ips.into_iter().map(|ip|(ip, 22).into()).collect();
|
|
||||||
// }
|
|
||||||
// Err(_) => "off",
|
|
||||||
// }
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
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(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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn status_build(machine_name: &str) -> String {
|
|
||||||
let formatted_macs = match get_macs_2_1(machine_name).await {
|
|
||||||
Ok(table) => {
|
|
||||||
let the: String = table
|
|
||||||
.iter()
|
|
||||||
.map(|(ip, mac, state)| {
|
|
||||||
let mac_str = // if let Some(mac) = mac {
|
|
||||||
mac.to_string()
|
|
||||||
// } else {
|
|
||||||
// "None".into()
|
|
||||||
// }
|
|
||||||
;
|
|
||||||
format!(
|
|
||||||
"<tr><td>{ip}</td><td>{mac_str}</td><td>{state}</td></tr>",
|
|
||||||
state = state.dumber_state()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
format!(
|
|
||||||
r#"<p>info of {machine_name}:</p>
|
|
||||||
<table>
|
|
||||||
<tr><th>IP</th><th>MAC</th><th>State</th></tr>
|
|
||||||
{the}
|
|
||||||
</table>"#
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Err(e) => format!("<p>errors getting table for {machine_name}: {e}</p>"),
|
|
||||||
};
|
|
||||||
|
|
||||||
format!(
|
|
||||||
r#"
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
{formatted_macs}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn entry() -> io::Result<()> {
|
async fn entry() -> io::Result<()> {
|
||||||
use crate::route::{api_status, devs_router, get_status_2, home_2, home_2_route};
|
use crate::route::{api_router, home_2, home_2_route, wake_handler};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/home", get(home))
|
// .route("/home", get(home))
|
||||||
.route("/", get(home_2))
|
.route("/", get(home_2))
|
||||||
.merge(home_2_route())
|
.merge(home_2_route())
|
||||||
.route("/wake", post(wake_handler))
|
.route("/wake", post(wake_handler))
|
||||||
.route("/status", get(get_status_2))
|
// .route("/status", get(get_status_2))
|
||||||
.merge(api_status())
|
.nest("/api", api_router());
|
||||||
.route("/api/devs", get(devs_router));
|
|
||||||
|
|
||||||
let port = TcpListener::bind("0.0.0.0:12012").await?;
|
let port = TcpListener::bind("0.0.0.0:12012").await?;
|
||||||
axum::serve(port, app.into_make_service()).await?;
|
axum::serve(port, app.into_make_service()).await?;
|
||||||
@@ -119,7 +33,6 @@ async fn entry() -> io::Result<()> {
|
|||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
fn main() -> color_eyre::Result<()> {
|
fn main() -> color_eyre::Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
use std::net::ToSocketAddrs;
|
|
||||||
// use crate::arpparse::NUDState;
|
// use crate::arpparse::NUDState;
|
||||||
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
|
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
|
||||||
println!("{:?}", "svuhuvshdv:331".to_socket_addrs());
|
println!("{:?}", "svuhuvshdv:331".to_socket_addrs());
|
||||||
|
|||||||
+47
-199
@@ -1,30 +1,20 @@
|
|||||||
use std::collections::HashSet;
|
pub mod api;
|
||||||
use std::net::IpAddr;
|
|
||||||
|
pub use crate::route::api::{DeviceQuery, api_router};
|
||||||
|
use crate::{
|
||||||
|
r#static as st,
|
||||||
|
utils::{ping::_ping_ip, wake::wake},
|
||||||
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Router,
|
||||||
extract::Path,
|
|
||||||
http::{StatusCode, header},
|
http::{StatusCode, header},
|
||||||
response::{Html, IntoResponse, Redirect},
|
response::{Html, IntoResponse},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Query;
|
use axum_extra::extract::Query;
|
||||||
use macaddr::MacAddr;
|
|
||||||
use serde::ser::Serializer;
|
|
||||||
use serde_with::skip_serializing_none;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{MACHINE_NAME, utils::_status_build};
|
||||||
MACHINE_NAME,
|
|
||||||
arpparse::{self, NUDState},
|
|
||||||
st, status_build,
|
|
||||||
utils::{
|
|
||||||
de_many,
|
|
||||||
query::{
|
|
||||||
dev::{self, has_dev},
|
|
||||||
get_macs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn home_2() -> Html<&'static str> {
|
pub async fn home_2() -> Html<&'static str> {
|
||||||
Html(st::HOME_2)
|
Html(st::HOME_2)
|
||||||
@@ -56,197 +46,55 @@ pub fn home_2_route() -> Router {
|
|||||||
.route("/home_2.js", get(home_2_js)) //js
|
.route("/home_2.js", get(home_2_js)) //js
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
|
||||||
pub struct DeviceQuery {
|
|
||||||
pub name: Option<String>,
|
|
||||||
// Accept single or many; ignore blanks
|
|
||||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
|
||||||
ip: Vec<IpAddr>,
|
|
||||||
#[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>,
|
|
||||||
/// optional NUD state filter; accepts any case (e.g., reachable, REACHABLE)
|
|
||||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
|
||||||
pub nud: Vec<NUDState>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
|
||||||
pub struct NamePath {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Debug, Default, serde::Serialize)]
|
|
||||||
pub struct Status {
|
|
||||||
name: Option<String>,
|
|
||||||
table: Vec<arpparse::IpNeighLine>,
|
|
||||||
filters: Filters,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, serde::Serialize)]
|
pub async fn wake_handler(
|
||||||
pub struct Filters {
|
Query(DeviceQuery { name, .. }): Query<DeviceQuery>,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
) -> axum::response::Result<impl IntoResponse> {
|
||||||
ip: Vec<IpAddr>,
|
match wake(name.as_deref().unwrap_or(MACHINE_NAME)).await {
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
Ok(0) => Err((StatusCode::NOT_FOUND, "No packets sent!").into()),
|
||||||
dev: Vec<String>,
|
Ok(x) => Ok((
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
StatusCode::ACCEPTED,
|
||||||
nud: Vec<NUDState>,
|
format!("{x} packet{s} sent!", s = if x > 1 { "s" } else { "" }),
|
||||||
#[serde(
|
)),
|
||||||
skip_serializing_if = "Vec::is_empty",
|
_ => Err((StatusCode::GATEWAY_TIMEOUT, "Wake failed").into()),
|
||||||
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)]
|
|
||||||
pub struct StatusError {
|
|
||||||
name: Option<String>,
|
|
||||||
error: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_status_json(
|
|
||||||
// p: Option<Path<NamePath>>,
|
|
||||||
Query(DeviceQuery {
|
|
||||||
name,
|
|
||||||
ip,
|
|
||||||
dev,
|
|
||||||
nud,
|
|
||||||
mac,
|
|
||||||
..
|
|
||||||
}): Query<DeviceQuery>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> {
|
|
||||||
if slice.is_empty() {
|
|
||||||
vec![None]
|
|
||||||
} else {
|
|
||||||
slice.iter().cloned().map(Some).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* let name = /* p
|
|
||||||
.map(|Path(n)| n.name)
|
|
||||||
.or */(name)
|
|
||||||
// .unwrap_or_else(|| MACHINE_NAME.to_owned())
|
|
||||||
; */
|
|
||||||
// ip/dev/nud already parsed; assemble options
|
|
||||||
let ips_opt = if ip.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(ip.clone())
|
|
||||||
};
|
|
||||||
let dev_opts: Vec<Option<String>> = to_opts(&dev);
|
|
||||||
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
|
|
||||||
|
|
||||||
let filters = Filters {
|
|
||||||
ip,
|
|
||||||
dev,
|
|
||||||
nud,
|
|
||||||
mac: mac.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run combinations of dev/nud and merge results
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
for d in &dev_opts {
|
|
||||||
for n in &nud_opts {
|
|
||||||
tasks.push(get_macs(
|
|
||||||
name.as_deref(),
|
|
||||||
ips_opt.as_deref(),
|
|
||||||
d.as_deref(),
|
|
||||||
*n,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match futures::future::try_join_all(tasks)
|
|
||||||
.await
|
|
||||||
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
// [(header::LINK, format!("<{canonical}>; rel=\"canonical\""))],
|
|
||||||
Json(Status {
|
|
||||||
name,
|
|
||||||
table,
|
|
||||||
filters,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
Err(error) => (
|
|
||||||
StatusCode::BAD_GATEWAY,
|
|
||||||
Json(StatusError {
|
|
||||||
name,
|
|
||||||
error: error.to_string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_response(), // holy clutch. Couldve been disasterous
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
|
|
||||||
Redirect::permanent(&format!(
|
|
||||||
"/api/status?name={name}",
|
|
||||||
name = urlencoding::encode(&name) // just for
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
pub async fn _get_status_2(q: Query<DeviceQuery>) -> Html<String> {
|
||||||
let name = match q {
|
let name = match q {
|
||||||
Query(DeviceQuery {
|
Query(DeviceQuery {
|
||||||
name: Some(name), ..
|
name: Some(name), ..
|
||||||
}) => name,
|
}) => name,
|
||||||
_ => MACHINE_NAME.to_string(),
|
_ => MACHINE_NAME.to_string(),
|
||||||
};
|
};
|
||||||
Html(status_build(&name).await)
|
Html(_status_build(&name).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly
|
pub async fn _home() -> Html<String> {
|
||||||
pub async fn status_smart_redirect(Path(q): Path<String>) -> Redirect {
|
Html(format!(
|
||||||
let s = q.trim();
|
r#"
|
||||||
// 1) IP
|
<html>
|
||||||
if let Ok(ip) = s.parse::<IpAddr>() {
|
<body>
|
||||||
return Redirect::to(&format!("/api/status?ip={ip}"));
|
<p><a href="/home_2">Alternate UI</a></p>
|
||||||
}
|
<p>the machine is {}! <a href="/status">Status</a></p>
|
||||||
// 2) MAC
|
<form method="POST" action="/wake">
|
||||||
if let Ok(mac) = s.parse::<MacAddr>() {
|
<button type="submit">Wake LDA</button>
|
||||||
return Redirect::to(&format!("/api/status?mac={mac}"));
|
</form>
|
||||||
}
|
</body>
|
||||||
// 3) NUD state (reachable, stale, ...)
|
</html>
|
||||||
if let Ok(state) = s.parse::<NUDState>() {
|
"#,
|
||||||
return Redirect::to(&format!("/api/status?nud={state}"));
|
if _ping_ip((MACHINE_NAME, 22)).await {
|
||||||
}
|
"on"
|
||||||
// 4) Known device? prefer dev first
|
} else {
|
||||||
if has_dev(s) {
|
"off"
|
||||||
return Redirect::to(&format!("/api/status?dev={}", urlencoding::encode(s)));
|
} // match get_ips(MACHINE_NAME).await {
|
||||||
}
|
// Ok(ips) => {
|
||||||
// 5) Try DNS: if it resolves, treat as name
|
// // let addrs: Vec<SocketAddr> = ips.into_iter().map(|ip|(ip, 22).into()).collect();
|
||||||
if tokio::net::lookup_host((s, 0)).await.is_ok() {
|
// }
|
||||||
return Redirect::to(&format!("/api/status?name={}", urlencoding::encode(s)));
|
// Err(_) => "off",
|
||||||
}
|
// }
|
||||||
// Default: name last
|
))
|
||||||
Redirect::to(&format!("/api/status?name={}", urlencoding::encode(s)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn devs_router() -> Json<Vec<String>> {
|
|
||||||
dev::devs_sorted().into()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
use crate::utils::de_many;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json, Router,
|
||||||
|
extract::Path,
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
routing::get,
|
||||||
|
};
|
||||||
|
use axum_extra::extract::Query;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::ser::Serializer;
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
arpparse::{self, NUDState},
|
||||||
|
dhcpparse,
|
||||||
|
utils::query::{
|
||||||
|
dev::{self, has_dev},
|
||||||
|
get_macs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_dhcp_leases() -> impl IntoResponse {
|
||||||
|
match tokio::task::spawn_blocking(dhcpparse::read_dhcp_leases).await {
|
||||||
|
Ok(Ok(leases)) => (StatusCode::OK, Json(leases)).into_response(),
|
||||||
|
Ok(Err(e)) => (
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
Json(StatusError {
|
||||||
|
name: None,
|
||||||
|
error: e.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(join_err) => (
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
Json(StatusError {
|
||||||
|
name: None,
|
||||||
|
error: join_err.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
||||||
|
pub struct DeviceQuery {
|
||||||
|
pub name: Option<String>,
|
||||||
|
// Accept single or many; ignore blanks
|
||||||
|
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
||||||
|
ip: Vec<IpAddr>,
|
||||||
|
#[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>,
|
||||||
|
/// optional NUD state filter; accepts any case (e.g., reachable, REACHABLE)
|
||||||
|
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
||||||
|
pub nud: Vec<NUDState>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
||||||
|
pub struct NamePath {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Default, serde::Serialize)]
|
||||||
|
pub struct Status {
|
||||||
|
name: Option<String>,
|
||||||
|
table: Vec<arpparse::IpNeighLine>,
|
||||||
|
filters: Filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Serialize)]
|
||||||
|
pub struct Filters {
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
ip: Vec<IpAddr>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
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)]
|
||||||
|
pub struct StatusError {
|
||||||
|
name: Option<String>,
|
||||||
|
error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_status_json(
|
||||||
|
// p: Option<Path<NamePath>>,
|
||||||
|
Query(DeviceQuery {
|
||||||
|
name,
|
||||||
|
ip,
|
||||||
|
dev,
|
||||||
|
nud,
|
||||||
|
mac,
|
||||||
|
..
|
||||||
|
}): Query<DeviceQuery>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> {
|
||||||
|
if slice.is_empty() {
|
||||||
|
vec![None]
|
||||||
|
} else {
|
||||||
|
slice.iter().cloned().map(Some).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* let name = /* p
|
||||||
|
.map(|Path(n)| n.name)
|
||||||
|
.or */(name)
|
||||||
|
// .unwrap_or_else(|| MACHINE_NAME.to_owned())
|
||||||
|
; */
|
||||||
|
// ip/dev/nud already parsed; assemble options
|
||||||
|
let ips_opt = if ip.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ip.clone())
|
||||||
|
};
|
||||||
|
let dev_opts: Vec<Option<String>> = to_opts(&dev);
|
||||||
|
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
|
||||||
|
|
||||||
|
let filters = Filters {
|
||||||
|
ip,
|
||||||
|
dev,
|
||||||
|
nud,
|
||||||
|
mac: mac.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run combinations of dev/nud and merge results
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
for d in &dev_opts {
|
||||||
|
for n in &nud_opts {
|
||||||
|
tasks.push(get_macs(
|
||||||
|
name.as_deref(),
|
||||||
|
ips_opt.as_deref(),
|
||||||
|
d.as_deref(),
|
||||||
|
*n,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match futures::future::try_join_all(tasks)
|
||||||
|
.await
|
||||||
|
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
// [(header::LINK, format!("<{canonical}>; rel=\"canonical\""))],
|
||||||
|
Json(Status {
|
||||||
|
name,
|
||||||
|
table,
|
||||||
|
filters,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
Err(error) => (
|
||||||
|
StatusCode::BAD_GATEWAY,
|
||||||
|
Json(StatusError {
|
||||||
|
name,
|
||||||
|
error: error.to_string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(), // holy clutch. Couldve been disasterous
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
|
||||||
|
Redirect::permanent(&format!(
|
||||||
|
"/api/status?name={name}",
|
||||||
|
name = urlencoding::encode(&name) // just for
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/status/{name}", get(status_redirect))
|
||||||
|
.route("/status", get(get_status_json))
|
||||||
|
.route("/dhcp_leases", get(get_dhcp_leases))
|
||||||
|
.route("/smart/{q}", get(status_smart_redirect))
|
||||||
|
.route("/devs", get(devs_router))
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@
|
|||||||
pub mod wake;
|
pub mod wake;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use crate::utils::query::_get_macs_2_1;
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
||||||
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
|
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
|
||||||
@@ -71,3 +73,43 @@ pub mod de_many {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn _status_build(machine_name: &str) -> String {
|
||||||
|
let formatted_macs = match _get_macs_2_1(machine_name).await {
|
||||||
|
Ok(table) => {
|
||||||
|
let the: String = table
|
||||||
|
.iter()
|
||||||
|
.map(|(ip, mac, state)| {
|
||||||
|
let mac_str = // if let Some(mac) = mac {
|
||||||
|
mac.to_string()
|
||||||
|
// } else {
|
||||||
|
// "None".into()
|
||||||
|
// }
|
||||||
|
;
|
||||||
|
format!(
|
||||||
|
"<tr><td>{ip}</td><td>{mac_str}</td><td>{state}</td></tr>",
|
||||||
|
state = state._dumber_state()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
format!(
|
||||||
|
r#"<p>info of {machine_name}:</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>IP</th><th>MAC</th><th>State</th></tr>
|
||||||
|
{the}
|
||||||
|
</table>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(e) => format!("<p>errors getting table for {machine_name}: {e}</p>"),
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
{formatted_macs}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ use tokio::{
|
|||||||
time::timeout,
|
time::timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
|
pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
|
||||||
timeout(Duration::from_secs(1), TcpStream::connect(addr))
|
timeout(Duration::from_secs(1), TcpStream::connect(addr))
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
|
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
|
||||||
Ok(get_macs_1(machine_name)
|
Ok(get_macs_1(machine_name)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
Reference in New Issue
Block a user