chatgpt lowkey goated
This commit is contained in:
+1
-1
@@ -114,7 +114,7 @@ impl NUDState {
|
||||
}
|
||||
}
|
||||
/// dumb UI label
|
||||
pub fn dumber_state(&self) -> &'static str {
|
||||
pub fn _dumber_state(&self) -> &'static str {
|
||||
match self {
|
||||
NUDState::Permanent | NUDState::Reachable => "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::{
|
||||
Router,
|
||||
extract::Query,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse},
|
||||
routing::{get, post},
|
||||
};
|
||||
use tokio::net::TcpListener;
|
||||
mod arpparse;
|
||||
mod dhcpparse;
|
||||
mod route;
|
||||
mod r#static;
|
||||
pub mod r#static;
|
||||
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;
|
||||
|
||||
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")]
|
||||
#[tokio::main]
|
||||
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()
|
||||
.route("/home", get(home))
|
||||
// .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())
|
||||
.route("/api/devs", get(devs_router));
|
||||
// .route("/status", get(get_status_2))
|
||||
.nest("/api", api_router());
|
||||
|
||||
let port = TcpListener::bind("0.0.0.0:12012").await?;
|
||||
axum::serve(port, app.into_make_service()).await?;
|
||||
@@ -119,7 +33,6 @@ async fn entry() -> io::Result<()> {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
use std::net::ToSocketAddrs;
|
||||
// use crate::arpparse::NUDState;
|
||||
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
|
||||
println!("{:?}", "svuhuvshdv:331".to_socket_addrs());
|
||||
|
||||
+47
-199
@@ -1,30 +1,20 @@
|
||||
use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
pub mod api;
|
||||
|
||||
pub use crate::route::api::{DeviceQuery, api_router};
|
||||
use crate::{
|
||||
r#static as st,
|
||||
utils::{ping::_ping_ip, wake::wake},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::Path,
|
||||
Router,
|
||||
http::{StatusCode, header},
|
||||
response::{Html, IntoResponse, Redirect},
|
||||
response::{Html, IntoResponse},
|
||||
routing::get,
|
||||
};
|
||||
use axum_extra::extract::Query;
|
||||
use macaddr::MacAddr;
|
||||
use serde::ser::Serializer;
|
||||
use serde_with::skip_serializing_none;
|
||||
|
||||
use crate::{
|
||||
MACHINE_NAME,
|
||||
arpparse::{self, NUDState},
|
||||
st, status_build,
|
||||
utils::{
|
||||
de_many,
|
||||
query::{
|
||||
dev::{self, has_dev},
|
||||
get_macs,
|
||||
},
|
||||
},
|
||||
};
|
||||
use crate::{MACHINE_NAME, utils::_status_build};
|
||||
|
||||
pub async fn home_2() -> Html<&'static str> {
|
||||
Html(st::HOME_2)
|
||||
@@ -56,197 +46,55 @@ pub fn home_2_route() -> Router {
|
||||
.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 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 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_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 {
|
||||
Query(DeviceQuery {
|
||||
name: Some(name), ..
|
||||
}) => name,
|
||||
_ => 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 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 _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",
|
||||
// }
|
||||
))
|
||||
}
|
||||
|
||||
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;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::utils::query::_get_macs_2_1;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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))
|
||||
.await
|
||||
.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)
|
||||
.await?
|
||||
.into_iter()
|
||||
|
||||
Reference in New Issue
Block a user