chatgpt lowkey goated

This commit is contained in:
lda
2025-08-26 05:41:46 +07:00 Unverified
parent bfd1b626d5
commit f4fcf7be65
8 changed files with 372 additions and 295 deletions
+1 -1
View File
@@ -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",
+41
View File
@@ -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
View File
@@ -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
View File
@@ -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 { pub async fn _get_status_2(q: Query<DeviceQuery>) -> Html<String> {
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> {
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()
}
+233
View File
@@ -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))
}
+42
View File
@@ -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
View File
@@ -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
View File
@@ -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()