51 Commits
75 changed files with 2445 additions and 1389 deletions

No files matched your search

+7 -2
View File
@@ -1,5 +1,10 @@
[target.armv7-unknown-linux-musleabihf]
linker = "rust-lld"
[build]
target = "armv7-unknown-linux-musleabihf"
[target.armv7-unknown-linux-musleabihf]
linker = "rust-lld"
[alias]
ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
t = "test -- --nocapture --test-threads=1"
tdebug = "test -- --nocapture --test-threads=1 --show-output"
+3 -1
View File
@@ -1,8 +1,10 @@
you should use block code to refence code, like so:
```rust
hello_world!(println);
// indented lines
```
in the chat. Else it fucks up formatting.
We work towards clean, maintainable design. im not the best at this thing.
We work towards clean, maintainable design. im not the best at this thing.
+3 -2
View File
@@ -1,3 +1,4 @@
{
"rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf"
}
"rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf",
// "rust-analyzer.diagnostics.disabled": ["unlinked-file"]
}
Generated
+432 -228
View File
File diff suppressed because it is too large. Load diff
+32 -15
View File
@@ -1,23 +1,31 @@
[package]
name = "wakey"
version = "0.1.4"
version = "0.1.7"
edition = "2024"
publish = ["gitea"]
[dependencies]
axum = { version = "0.8.4", features = ["macros"] }
axum-extra = { version = "0.10.1", features = ["query"] }
color-eyre = "0.6.5"
futures = "0.3.31"
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_html_form = "0.2.7"
serde_json = "1.0.143"
serde_with = { version = "3.14.0", features = ["json"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
thiserror = "2.0.16"
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] }
urlencoding = "2.1.3"
anyhow = "1"
axum = { version = "0", features = ["macros"] }
axum-extra = { version = "0", features = ["query"] }
color-eyre = "0"
futures = "0"
macaddr = { version = "1", features = ["serde", "serde_std"] }
serde = { version = "1", features = ["derive"] }
serde_html_form = "0"
serde_json = "1"
serde_with = { version = "3", features = ["json"] }
strum = { version = "0", features = ["derive", "strum_macros"] }
thiserror = "2"
tokio = { version = "1", features = [
"fs",
"process",
"rt-multi-thread",
"io-util",
"macros",
] }
tower-http = { version = "0", features = ["fs"] }
urlencoding = "2"
[profile.release]
opt-level = "z"
@@ -25,4 +33,13 @@ strip = true
[features]
default = ["very-smart-parsing"]
very-smart-parsing = [] # this is the a-bit-redundant parse thing that copilot made
very-smart-parsing = [
] # this is the a-bit-redundant parse thing that copilot made
[workspace]
members = ["ipjs"]
[dependencies.lda-ipjs]
path = "ipjs"
registry = "gitea"
version = "*"
+10 -13
View File
@@ -2,19 +2,16 @@
router shit
## usage
## whatever i did must be BEAUTIFUL
- Build (Windows host OK):
- cargo build --release --target armv7-unknown-linux-musleabihf
- Run on a Linux box/router:
- copy target/armv7-unknown-linux-musleabihf/release/wakey to the device
- chmod +x wakey && ./wakey
- Install via release asset (OpenWrt):
- wget -O- `https://your.gitea/owner/repo/releases/download/vX.Y.Z/wakey-rootfs-vX.Y.Z-armv7-unknown-linux-musleabihf.tgz` | tar -xz -C /
- /etc/init.d/wakey enable && /etc/init.d/wakey start
read [in vietnamese](https://ldlda.com/blogs/else/ssh-ax3000cv2-update-firmware-0-10-2)
Optional updater (fetch latest automatically on OpenWrt):
## now i NEED the split arch to be on
```sh
WAKEY_HOST=git.ldlda.com WAKEY_OWNER=lda WAKEY_REPO=wakey sh /etc/ldlda_help/update_wakey.sh
```
like RN rn rn so i can have web outside and the lil agent can be sitting here configuring itself (Parse/append to rc.local files? add to init.d? fuhh)
I NEED IT
I NEEED IT
I NEEEEEED it.
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "lda-ipjs"
description = "ip -j show schemas"
version = "0.0.2"
edition = "2024"
publish = ["gitea"]
[dependencies]
macaddr = { version = "1", features = ["serde", "serde_std"] }
strum = { version = "0", features = ["derive", "strum_macros"] }
serde_json = "1"
serde = { version = "1", features = ["derive"] }
serde_with = { version = "3", features = ["json"] }
thiserror = "2"
anyhow = "1"
tokio = { version = "1", features = ["fs", "process", "rt-multi-thread", "io-util", "macros"] }
rtnetlink = "0"
futures = "0"
+3
View File
@@ -0,0 +1,3 @@
trait IpCommand {
}
View File
Whitespace-only changes.
+20
View File
@@ -0,0 +1,20 @@
//! low
//!
//! # lda-ipjs
//!
//! this package will represent all my needs with the all the subcommands of ip -j.
//!
//! ## what i need
//!
//! ```console
//! ip -j neigh show
//! ```
//!
//! i also need to see devices and idk MAYBE maybe not MAYBE UHHHHHH maybe broadcast
//!
//! LOWK if this were to be calls to kernel or some bullshit then PLEASE because doing ts parsing its hell cuh
pub mod subcommands;
pub mod utils;
// i want a generalized way to build and call
+46
View File
@@ -0,0 +1,46 @@
// ip address [ show [ dev IFNAME ] [ scope SCOPE-ID ] [ master DEVICE ]
// [ type TYPE ] [ to PREFIX ] [ FLAG-LIST ]
// [ label LABEL ] [up] [ vrf NAME ] ]
// fuck is this mean
// do i need all this? do i need anything but `ip -j a show dev br-lan`?
// TYPE := { vlan | veth | vcan | vxcan | dummy | ifb | macvlan | macvtap |
// bridge | bond | ipoib | ip6tnl | ipip | sit | vxlan | lowpan |
// gre | gretap | erspan | ip6gre | ip6gretap | ip6erspan | vti |
// nlmon | can | bond_slave | ipvlan | geneve | bridge_slave |
// hsr | macsec | netdevsim }
// FLAG-LIST := [ FLAG-LIST ] FLAG
// FLAG := [ permanent | dynamic | secondary | primary |
// [-]tentative | [-]deprecated | [-]dadfailed | temporary |
// CONFFLAG-LIST ]
// CONFFLAG-LIST := [ CONFFLAG-LIST ] CONFFLAG
// CONFFLAG := [ home | nodad | mngtmpaddr | noprefixroute | autojoin ]
// prefix seems to be a cidr. both 6 and 4 works. idfk dog
use std::{io, process::Output};
use anyhow::Context;
use super::AddrOutput;
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
let output = _get(dev).await.context("Can not run command")?;
if !output.status.success() {
anyhow::bail!(String::from_utf8_lossy(&output.stderr).into_owned());
}
serde_json::from_slice(&output.stdout).context("Deserialize failed")
}
pub async fn _get(dev: Option<&str>) -> io::Result<Output> {
let mut cmd = tokio::process::Command::new("ip");
cmd.args(["-j", "address", "show"]);
if let Some(d) = dev {
cmd.args(["dev", d]);
}
cmd.output().await
}
+42
View File
@@ -0,0 +1,42 @@
//! ts
//!
//! deals with both ip a (addroutput) and ip l (commonoutput)
//!
//! lowk why its free but its indirection and its ass
pub mod json;
pub mod nl;
use crate::utils::serialize::mac::option_mac;
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
/// i dont include what i dont know about (almost all ts)
#[derive(Serialize, Debug, Deserialize)]
pub struct AddrOutput {
pub ifindex: u32,
pub ifname: String,
/// i imagine UP or DOWN, unknown
pub operstate: String,
// 6 has a serde and the enum doesnt? why. (serializing ts is ass although... im not given an array. they string formatted ts)
#[serde(with = "option_mac", default)]
pub address: Option<MacAddr>,
#[serde(default)] // i wish we have intellisense for this... fuck you metaprogramming
pub addr_info: Vec<AddrInfo>,
}
// i be copying
// Raw JSON shape from ip -j -4 address show
#[derive(Debug, Deserialize, Serialize)]
pub struct AddrInfo {
pub family: Option<String>,
pub local: Option<String>,
pub prefixlen: Option<u8>,
pub broadcast: Option<String>,
pub scope: Option<String>,
pub label: Option<String>,
// many more exist; we only take what we need
}
+22
View File
@@ -0,0 +1,22 @@
//! i said i aint doing ts no more why am i still here
use futures::TryStreamExt;
use crate::subcommands::address::AddrOutput;
// shit this one is even worse you needa collect info from two places
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
let (conn, handle, _) = rtnetlink::new_connection()?;
tokio::spawn(conn); // every time?
let mut address = handle.address().get();
let mut link = handle.link().get();
if let Some(dev) = dev {
link = link.match_name(dev.to_owned());
if let Some(ind) = link.execute().try_next().await?.map(|a| a.header.index) {
address = address.set_link_index_filter(ind);
};
address.execute().try_next().await?;
} else {
};
todo!();
}
+1
View File
@@ -0,0 +1 @@
//! this is for link. You need link; at least to build an index -> name map. i Need It. sometimes.
+2
View File
@@ -0,0 +1,2 @@
pub mod address;
pub mod neighbor;
+60
View File
@@ -0,0 +1,60 @@
//! idk what to put here
use std::{io, net::IpAddr, process::Output};
use anyhow::{Context, bail};
use super::{NUDState, NeighborItem};
// loose translation of [wakey::utils::query::macs::get_mac]
// i think ill write tokio::process every time tho (for this if let thing) because iterate through all ts youll have to as str and all the hooplas.
// it all turns to live osstr tho so ts just for my own sanity
// thiserror? anyhow
// what is vro sayin
// ahh. instead of using [wakey::utils::cmd::exec_command] which is ass we jus write everything out. so i dont have to .as_str() so often.
pub async fn get(
ip: Option<IpAddr>,
dev: Option<&str>,
nud: &[NUDState],
) -> anyhow::Result<Vec<NeighborItem>> {
let output = _get(ip, dev, nud).await.context("Can not run command")?;
if !output.status.success() {
bail!(String::from_utf8_lossy(&output.stderr).into_owned())
} else {
let mut fuckass: Vec<NeighborItem> =
serde_json::from_slice(&output.stdout).context("Deserialize failed")?;
// i hate ts.
if let Some(dev) = dev {
for item in &mut fuckass {
if item.dev.is_none() {
item.dev = Some(dev.to_owned());
}
}
};
Ok(fuckass)
}
}
pub async fn _get(ip: Option<IpAddr>, dev: Option<&str>, nud: &[NUDState]) -> io::Result<Output> {
let mut cmd = tokio::process::Command::new("ip");
cmd.args(["-j", "neigh", "show"]);
if let Some(ip) = ip {
// cmd.arg("to");
cmd.arg(ip.to_canonical().to_string());
};
if let Some(dev) = dev {
cmd.args(["dev", dev]);
}
for nud in nud {
cmd.arg("nud");
cmd.arg(nud.to_string());
}
cmd.output().await
}
+82
View File
@@ -0,0 +1,82 @@
//! ```bash
//! ip -j n s
//! ```
//!
//! yes. this is a real call.
pub mod json;
pub mod nl;
use crate::utils::serialize::mac::option_mac;
use std::net::IpAddr;
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborInput {
/// supports only the last item (ignore), `to` keyword is optional
pub to: Option<IpAddr>,
/// supports only one item (it complains if multiple)
pub dev: Option<String>, // im all for simplicity
/// takes multiple, has to have `nud` before bro or it will think you `to`
pub nud: Vec<NUDState>,
}
// as input this must be lowercase. as output it is uppercase
/// docs for items come from a random ahh man website idk
#[derive(
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default,
)]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
#[serde(rename_all = "UPPERCASE")]
pub enum NUDState {
/// the neighbour entry is valid forever and can
/// be only be removed administratively.
Permanent,
/// the neighbour entry is valid. No attempts to
/// validate this entry will be made but it can
/// be removed when its lifetime expires.
Noarp,
/// the neighbour entry is valid until the
/// reachability timeout expires.
Reachable,
/// the neighbour entry is valid but suspicious.
/// This option to ip neigh does not change the
/// neighbour state if it was valid and the
/// address is not changed by this command.
Stale,
/// this is a pseudo state used when initially
/// creating a neighbour entry or after trying to
/// remove it before it becomes free to do so.
#[default]
None,
/// the neighbour entry has not (yet) been
/// validated/resolved.
Incomplete,
/// neighbor entry validation is currently
/// delayed.
Delay,
/// neighbor is being probed.
Probe,
/// max number of probes exceeded without
/// success, neighbor validation has ultimately
/// failed.
Failed,
Other(u16),
}
/// everything i see
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
pub struct NeighborItem {
#[serde(rename(deserialize = "dst"))]
pub ip: IpAddr,
#[serde(default)]
pub dev: Option<String>,
#[serde(with = "option_mac", default, rename(deserialize = "lladdr"))]
pub mac: Option<MacAddr>,
#[serde(default)]
pub state: Vec<NUDState>,
}
+145
View File
@@ -0,0 +1,145 @@
//! rtnetlink-based neighbor table query. One syscall, filter in userspace.
use std::{
collections::{HashMap, HashSet},
net::IpAddr,
};
use futures::TryStreamExt;
use macaddr::MacAddr;
use rtnetlink::packet_route::{
AddressFamily,
link::LinkAttribute,
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
};
use super::{NUDState, NeighborItem};
/// Fetch neighbors via rtnetlink. Empty slice = no filter (match all).
/// Non-empty slice = match ANY in the set.
// https://github.com/rust-netlink/rtnetlink/blob/main/examples/get_neighbours.rs
pub async fn get(
ips: &[IpAddr],
devs: &[impl AsRef<str>],
nuds: &[NUDState],
macs: &[MacAddr],
) -> anyhow::Result<Vec<NeighborItem>> {
let (conn, handle, _) = rtnetlink::new_connection()?;
tokio::spawn(conn);
let mut neighbor_data = handle.neighbours().get().execute();
// Build filter sets (empty = match all)
let ip_set: HashSet<IpAddr> = ips.iter().copied().collect();
let dev_set: HashSet<&str> = devs.iter().map(AsRef::as_ref).collect();
let nud_set: HashSet<&NUDState> = nuds.iter().collect();
let mac_set: HashSet<MacAddr> = macs.iter().copied().collect();
// Cache ifindex -> name
let mut ifname_cache: HashMap<u32, String> = HashMap::new();
let mut result = vec![];
'row: while let Some(msg) = neighbor_data.try_next().await? {
// Only IPv4/IPv6, skip NOARP
if !matches!(
msg.header.family,
AddressFamily::Inet | AddressFamily::Inet6
) || matches!(msg.header.state, NeighbourState::Noarp)
{
continue 'row;
}
let state: NUDState = msg.header.state.try_into().unwrap_or_default();
let mut ip = None;
let mut mac = None;
for attr in msg.attributes {
match attr {
NeighbourAttribute::Destination(addr) => match addr {
NeighbourAddress::Inet(v4) => ip = Some(IpAddr::from(v4)),
NeighbourAddress::Inet6(v6) => ip = Some(IpAddr::from(v6)),
_ => continue 'row,
},
NeighbourAttribute::LinkLocalAddress(bytes) => {
mac = match bytes.len() {
6 => bytes.first_chunk::<6>().map(|&b| MacAddr::from(b)),
8 => bytes.first_chunk::<8>().map(|&b| MacAddr::from(b)),
_ => continue 'row,
}
}
_ => {}
}
}
// Resolve ifindex -> name (cached)
let dev = match ifname_cache.get(&msg.header.ifindex) {
Some(name) => Some(name.clone()),
None => {
let name = handle
.link()
.get()
.match_index(msg.header.ifindex)
.execute()
.try_next()
.await?
.and_then(|link| {
link.attributes.into_iter().find_map(|a| match a {
LinkAttribute::IfName(n) => Some(n),
_ => None,
})
});
if let Some(ref n) = name {
ifname_cache.insert(msg.header.ifindex, n.clone());
}
name
}
};
let (Some(ip), Some(dev)) = (ip, dev) else {
continue 'row;
};
// Apply filters (empty set = match all)
if !ip_set.is_empty() && !ip_set.contains(&ip) {
continue 'row;
}
if !dev_set.is_empty() && !dev_set.contains(dev.as_str()) {
continue 'row;
}
if !nud_set.is_empty() && !nud_set.contains(&state) {
continue 'row;
}
if !mac_set.is_empty() && !mac.is_some_and(|m| mac_set.contains(&m)) {
continue 'row;
}
result.push(NeighborItem {
ip,
dev: Some(dev),
mac,
state: vec![state],
});
}
Ok(result)
}
impl TryFrom<NeighbourState> for NUDState {
fn try_from(value: NeighbourState) -> Result<Self, Self::Error> {
match value {
NeighbourState::Incomplete => Ok(Self::Incomplete),
NeighbourState::Reachable => Ok(Self::Reachable),
NeighbourState::Stale => Ok(Self::Stale),
NeighbourState::Delay => Ok(Self::Delay),
NeighbourState::Probe => Ok(Self::Probe),
NeighbourState::Failed => Ok(Self::Failed),
NeighbourState::Noarp => Ok(Self::Noarp),
NeighbourState::Permanent => Ok(Self::Permanent),
NeighbourState::None => Ok(Self::None),
NeighbourState::Other(e) => Ok(Self::Other(e)),
_ => Err(u16::MAX),
}
}
type Error = u16;
}
+35
View File
@@ -0,0 +1,35 @@
use std::iter::Fuse;
/// wrap ts into a [Fuse][std::iter::Fuse] or something
pub struct Real<A: Clone, B: Iterator<Item = A>> {
prepend: A,
iter: B,
my_turn: bool,
}
impl<A: Clone, B: Iterator<Item = A>> Real<A, B> {
pub fn new(prepend: A, iter: B) -> Self {
Self {
prepend,
iter,
my_turn: true,
}
}
pub fn fuse(prepend: A, iter: B) -> Fuse<Self> {
Self::new(prepend, iter).fuse()
}
}
impl<A: Clone, B: Iterator<Item = A>> Iterator for Real<A, B> {
type Item = A;
fn next(&mut self) -> Option<Self::Item> {
let a = if self.my_turn {
Some(self.prepend.clone())
} else {
self.iter.next()
};
self.my_turn = !self.my_turn;
a
}
}
+2
View File
@@ -0,0 +1,2 @@
// pub mod iter;
pub mod serialize;
+50
View File
@@ -0,0 +1,50 @@
use macaddr::MacAddr;
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
use serde::{Serialize, Serializer};
pub 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)
}
/// Serialize a MacAddr as a string
pub fn serialize<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&mac.to_string())
}
/// Deserialize a MacAddr from a string
pub fn deserialize<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
where
D: Deserializer<'de>,
{
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
s.parse::<MacAddr>().map_err(DeError::custom)
}
pub mod option_mac {
use super::*;
/// serialize an [`Option<MacAddr>`]
pub fn serialize<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
}
/// deserialize an [`Option<MacAddr>`], returns None for invalid input (::, 0.0.0.0)
pub fn deserialize<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: Option<&str> = Option::<&str>::deserialize(des)?;
match s {
Some(val) => match val.parse::<MacAddr>() {
Ok(mac) => Ok(Some(mac)),
Err(_) => Ok(None),
},
None => Ok(None),
}
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod mac;
// pub mod vec;
+37
View File
@@ -0,0 +1,37 @@
use serde::Deserialize;
use serde::de;
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
pub fn deserialize<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let raw: OneOrMany<String> = OneOrMany::<String>::deserialize(des)?;
let mut out = Vec::new();
match raw {
OneOrMany::One(s) => {
let t = s.trim();
if !t.is_empty() {
out.push(t.parse().map_err(de::Error::custom)?);
}
}
OneOrMany::Many(vs) => {
for s in vs {
let t = s.trim();
if t.is_empty() {
continue;
}
out.push(t.parse().map_err(de::Error::custom)?);
}
}
}
Ok(out)
}
+139
View File
@@ -0,0 +1,139 @@
use std::collections::HashSet;
use lda_ipjs::subcommands::{address, neighbor};
#[tokio::test] // ← Use tokio::test instead of manual #[tokio::main]
async fn ball1() -> anyhow::Result<()> {
let result = neighbor::nl::get(&[], &[] as &[&str], &[], &[]).await?;
println!("netlink results: {:?}", result);
Ok(()) // ← Don't force error, let it succeed
}
#[tokio::test]
async fn ball2() -> anyhow::Result<()> {
let result = neighbor::json::get(None, None, &[]).await?;
println!("json results: {:?}", result);
Ok(())
}
// Add this to debug the raw JSON
#[tokio::test]
async fn ball_raw_json() -> anyhow::Result<()> {
let output = tokio::process::Command::new("ip")
.args(["-j", "neigh", "show"])
.output()
.await?;
let json = String::from_utf8_lossy(&output.stdout);
println!("Raw JSON:\n{}", json);
// Try to parse it
let parsed: Result<Vec<neighbor::NeighborItem>, _> = serde_json::from_slice(&output.stdout);
match parsed {
Ok(items) => println!("Parsed {} items", items.len()),
Err(e) => println!("Parse error: {}", e),
}
Ok(())
}
// Check what fields actually exist in the JSON
#[tokio::test]
async fn ball_field_analysis() -> anyhow::Result<()> {
let output = tokio::process::Command::new("ip")
.args(["-j", "neigh", "show"])
.output()
.await?;
let raw: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout)?;
println!("Found {} neighbor entries", raw.len());
// Collect all unique field names across all entries
let mut all_fields = std::collections::HashSet::new();
for (i, entry) in raw.iter().enumerate() {
if let Some(obj) = entry.as_object() {
println!("\nEntry {}: {} fields", i, obj.len());
for (key, value) in obj {
all_fields.insert(key.clone());
println!(" {}: {} = {:?}", key, value.type_name(), value);
}
}
}
println!("\n=== All unique fields seen ===");
for field in &all_fields {
println!(" - {}", field);
}
Ok(())
}
// Helper trait to get type name for JSON values
trait TypeName {
fn type_name(&self) -> &str;
}
impl TypeName for serde_json::Value {
fn type_name(&self) -> &str {
match self {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "bool",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
}
}
// Test filtering logic
#[tokio::test]
async fn ball_compare_backends() -> anyhow::Result<()> {
println!("=== JSON Backend ===");
let json_result = neighbor::json::get(None, None, &[]).await?;
println!("Got {} entries from JSON", json_result.len());
println!("\n=== Netlink Backend ===");
let nl_result = neighbor::nl::get(&[], &[] as &[&str], &[], &[]).await?;
println!("Got {} entries from netlink", nl_result.len());
// Compare counts
if json_result.len() != nl_result.len() {
println!(
"\n⚠️ Count mismatch! JSON: {}, Netlink: {}",
json_result.len(),
nl_result.len()
);
} else {
println!("\n✅ Both backends returned same count");
}
let a: HashSet<neighbor::NeighborItem> = HashSet::from_iter(json_result);
let b: HashSet<neighbor::NeighborItem> = HashSet::from_iter(nl_result);
println!(
"istg {len1} == {len2} or else",
len1 = a.len(),
len2 = b.len()
);
Ok(())
}
// #[tokio::test]
// async fn cidr_filter() {
// unimplemented!("never. i aint add what i dont need")
// }
#[tokio::test]
async fn ipjas() -> anyhow::Result<()> {
let cuh = address::json::get(None).await?;
println!("{cuh:#?}");
Ok(())
}
#[tokio::test]
async fn ipjas_raw() -> anyhow::Result<()> {
let cuh = address::json::_get(None).await?;
println!("{cuh:#?}");
Ok(())
}
+17
View File
@@ -0,0 +1,17 @@
# this does one thing
param(
[string]$PASSWD
)
# idk what this does it works like that then thats how it is
pscp.exe -l root -scp -pw $PASSWD -r 192.168.100.1:/etc/ldlda_help $PSScriptRoot
$initDir = Join-Path $PSScriptRoot 'init.d'
New-Item -ItemType Directory -Force -Path $initDir | Out-Null
# these are sum
$files = "update_wakey" , "wakey" , "update_tailscale" , "wireguard_setup"
$remote = $files.ForEach({ "/etc/init.d/$_" })
$remote | ForEach-Object {
pscp.exe -l root -scp -pw $PASSWD "192.168.100.1:$_" "$initDir\"
}
+8 -4
View File
@@ -10,7 +10,9 @@ param(
# Opt-in to non-interactive configure; by default we print the command for you to run manually.
[switch]$ForceConfigure,
# When -Attach, run in the foreground to see logs (good for first-time troubleshooting)
[switch]$Attach
[switch]$Attach,
# Run one job then exit (great for CI tag pushes)
[switch]$Once
)
$ErrorActionPreference = 'Stop'
@@ -50,11 +52,13 @@ if (-not (Test-Path $configFile)) {
}
}
if ($Attach) {
Write-Host "Starting act_runner (attached)... Ctrl+C to stop"
if ($Attach -or $Once) {
Write-Host "Starting act_runner $(if ($Once) {'(once mode)'} else {'(attached)'})... $(if (-not $Once) {'Ctrl+C to stop'})"
$configParent = Split-Path -Parent $configFile
Push-Location $configParent
& $RunnerPath daemon --config $configFile
$daemonArgs = @('daemon', '--config', $configFile)
if ($Once) { $daemonArgs += '--once' }
& $RunnerPath @daemonArgs
Pop-Location
}
else {
+18 -55
View File
@@ -1,4 +1,5 @@
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidDefaultValueSwitchParameter', 'Default true is intentional for fast dev loop')]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', "",
Justification = 'Default true is intentional for fast dev loop')]
param(
[string]$Pass,
[string]$HostName = "192.168.100.1",
@@ -14,23 +15,7 @@ param(
$ErrorActionPreference = 'Stop'
try { $PSStyle.OutputRendering = 'Host' } catch {}
function Invoke-Ext {
param($Exe, $Arguments, $Label)
$displayArgs = $Arguments.Clone()
for ($i = 0; $i -lt $displayArgs.Count; $i++) {
if ($displayArgs[$i] -eq '-pw' -and ($i + 1) -lt $displayArgs.Count) {
$displayArgs[$i + 1] = '****'
}
}
Write-Host ("[{0}] {1} {2}" -f $Label, $Exe, ($displayArgs -join ' ')) -ForegroundColor Cyan
$out = & $Exe @Arguments 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error ("{0} failed ({1}):`n{2}" -f $Label, $LASTEXITCODE, ($out -join "`n"))
throw ("{0} failed ({1})" -f $Label, $LASTEXITCODE)
}
return $out
}
. "$PSScriptRoot/lib.ps1"
function Get-DeployScript {
param($DeployPreferred, $DeployTmp, $RemoteTmp, $RemotePath, $RestartFlag)
@@ -42,43 +27,7 @@ if [ ! -x "`$DEPLOY" ] && [ -f $DeployTmp ]; then
DEPLOY=$DeployTmp
fi
sh "`$DEPLOY" $RemoteTmp $RemotePath $RestartFlag
"@ # -replace "`r", "" -replace "`r`n", "`n"
}
function Invoke-Scp {
param($Local, $Dest, $Pass, $HostKey, [switch]$Quiet)
if ($pscp = Get-Command pscp.exe -ErrorAction SilentlyContinue) {
$arguments = @('-scp')
if ($Quiet) { $arguments += '-q' }
if ($HostKey) { $arguments += @('-batch', '-hostkey', $HostKey) }
if ($Pass) { $arguments += @('-pw', $Pass) }
$arguments += @($Local, $Dest)
Invoke-Ext -Exe $pscp.Path -Arguments $arguments -Label 'scp'
}
else {
$arguments = @('-O')
if ($Quiet) { $arguments += '-q' }
$arguments += @($Local, $Dest)
Invoke-Ext -Exe 'scp' -Arguments $arguments -Label 'scp'
}
}
function Invoke-Ssh {
param($Cmd, $User, $Remote, $Pass, [switch]$Quiet)
$Cmd = $Cmd -replace "`r`n", "`n" -replace "`r", ""
if ($plink = Get-Command plink.exe -ErrorAction SilentlyContinue) {
$arguments = @('-batch', '-ssh')
if ($Pass) { $arguments += @('-pw', $Pass) }
$arguments += "$User@$Remote", $Cmd
Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh'
}
else {
$arguments = @()
if ($Quiet) { $arguments += '-q' }
$arguments += "$User@$Remote", $Cmd
Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh'
}
"@
}
#---- Main Flow ----
@@ -102,6 +51,20 @@ try {
# Push main binary
Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Quiet:$Quiet
# Push static assets
$localStatic = Join-Path $repoRoot "static"
if (Test-Path $localStatic) {
# Assuming RemotePath is like /root/.bin/wakey, we want /root/.bin/static
# So we push 'static' directory to /root/.bin/
$remoteDir = (Split-Path $RemotePath -Parent) -replace '\\', '/'
# Ensure remote dir exists (ssh mkdir -p)
Invoke-Ssh -Cmd "mkdir -p $remoteDir" -User $User -Remote $HostName -Pass $Pass -Quiet:$Quiet
# SCP -r static user@host:/root/.bin/
# Note: pscp/scp behavior: if dest is a dir, it copies the source dir INTO it.
Invoke-Scp -Local $localStatic -Dest "$User@${HostName}:$remoteDir/" -Pass $Pass -HostKey $HostKey -Quiet:$Quiet -Recurse
}
# Push deploy helper if exists
if (Test-Path $localDeploy) {
Invoke-Scp -Local $localDeploy -Dest "$User@${HostName}:$deployTmp" -Pass $Pass -HostKey $HostKey -Quiet:$Quiet
+18 -19
View File
@@ -9,23 +9,22 @@
START=50 # Run before Tailscale (typically START~80)
start() {
LOGFILE=/var/log/update_tailscale.log
MAX_RETRIES=10
RETRY_COUNT=0
# shellcheck disable=SC2016
nohup sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if fn; then
echo "[update_tailscale] Success at $(date)" >>"$LOGFILE"
exit 0
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "[update_tailscale] Retry $RETRY_COUNT at $(date)" >>"$LOGFILE"
sleep 5
done
echo "[update_tailscale] Too many retries. Exiting at $(date)" >>"$LOGFILE"
exit 1
' >>"$LOGFILE" 2>&1 &
do_update() {
MAX_RETRIES=10
RETRY_COUNT=0
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if /etc/ldlda_help/update_tailscale.sh; then
echo "[update_tailscale] Success at $(date)"
return 0
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "[update_tailscale] Retry $RETRY_COUNT at $(date)"
sleep 5
done
echo "[update_tailscale] Too many retries. Exiting at $(date)"
return 1
}
start() {
do_update >> /var/log/update_tailscale.log 2>&1 &
}
+18 -17
View File
@@ -1,23 +1,24 @@
#!/bin/sh /etc/rc.common
START=66
start() {
LOGFILE=/var/log/update_wakey.log
# does this work now fam
# exposal to wg is separate; so weon care
do_update() {
MAX_RETRIES=10
RETRY_COUNT=0
# shellcheck disable=SC2016
sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if /etc/ldlda_help/update_wakey.sh; then
echo "[update_wakey] Success at $(date)" >>"$LOGFILE"
exit 0
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "[update_wakey] Retry $RETRY_COUNT at $(date)" >>"$LOGFILE"
sleep 5
done
echo "[update_wakey] Too many retries. Exiting at $(date)" >>"$LOGFILE"
exit 1
' >>"$LOGFILE" 2>&1 &
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if /etc/ldlda_help/update_wakey.sh; then
echo "[update_wakey] Success at $(date)"
return 0
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "[update_wakey] Retry $RETRY_COUNT at $(date)"
sleep 5
done
echo "[update_wakey] Too many retries. Exiting at $(date)"
return 1
}
start() {
do_update >>/var/log/update_wakey.log 2>&1 &
}
+7 -7
View File
@@ -8,14 +8,14 @@ NAME=wakey
BIN=/root/.bin/wakey
start_service() {
procd_open_instance
procd_set_param command "$BIN"
procd_set_param respawn 5 1 0
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
procd_open_instance
procd_set_param command "$BIN"
procd_set_param respawn 5 1 0
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
stop_service() {
: # procd manages the process; nothing to do here
: # procd manages the process; nothing to do here
}
+2 -2
View File
@@ -6,7 +6,7 @@ pids="$(pidof wakey 2>/dev/null)"
# 2) Fallback: ps | awk (avoid matching awk/grep themselves)
if [ -z "$pids" ]; then
pids="$(ps w 2>/dev/null | awk '/\/\.bin\/wakey/ && $0 !~ /awk/ {print $1}')"
pids="$(ps w 2>/dev/null | awk '/\/\.bin\/wakey/ && $0 !~ /awk/ {print $1}')"
fi
[ -n "$pids" ] || exit 0
@@ -18,7 +18,7 @@ kill -TERM "$pids" 2>/dev/null || true
usleep 250000 # uhhh sleep is stupid
remain=""
for p in $pids; do
kill -0 "$p" 2>/dev/null && remain="$remain $p"
kill -0 "$p" 2>/dev/null && remain="$remain $p"
done
[ -z "$remain" ] || kill -KILL "$remain" 2>/dev/null || true
+8 -8
View File
@@ -3,22 +3,22 @@
set -e
# 0th step from lda is to move the pre assed resolv.conf to the real resolv.conf
if [ -f /tmp/resolv.conf ]; then
cp -af /tmp/resolv.conf /etc/resolv.conf
cp -af /tmp/resolv.conf /etc/resolv.conf
elif [ -f /rom/etc/resolv.conf ]; then
cp -af /rom/etc/resolv.conf /etc/resolv.conf
cp -af /rom/etc/resolv.conf /etc/resolv.conf
fi
if [ -x "$(which tailscale)" ]; then
tailscale update
exit 0
tailscale update
exit 0
fi
# 1. Fetch the latest version number from Tailscale's site
LATEST=$(curl -s https://pkgs.tailscale.com/stable/ | grep -Eo 'tailscale_[0-9\.]+_arm.tgz' | head -n1)
[ -z "$LATEST" ] && {
echo "❌ Couldn't find latest ARM binary."
exit 1
echo "❌ Couldn't find latest ARM binary."
exit 1
}
URL="https://pkgs.tailscale.com/stable/${LATEST}"
@@ -26,8 +26,8 @@ URL="https://pkgs.tailscale.com/stable/${LATEST}"
cd /var/tmp
if [ -f "$LATEST" ]; then
echo "file exists"
rm "$LATEST"
echo "file exists"
rm "$LATEST"
fi
echo "Downloading $LATEST..."
+88 -88
View File
@@ -24,113 +24,113 @@ STAGING="$TMPDIR/wakey-rootfs.$$.$ARCH"
log() { echo "[update_wakey] $*"; }
fail() {
echo "[update_wakey] ERROR: $*" >&2
exit 1
echo "[update_wakey] ERROR: $*" >&2
exit 1
}
http_get() {
# http_get <url>
if command -v uclient-fetch >/dev/null 2>&1; then
# uclient-fetch supports -O -, write to stdout
uclient-fetch -O - "$1" 2>/dev/null || return 1
elif command -v wget >/dev/null 2>&1; then
# shellcheck disable=SC2086
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fsSL -k "$1" || return 1
else
curl -fsSL "$1" || return 1
fi
else
return 2
fi
# http_get <url>
if command -v uclient-fetch >/dev/null 2>&1; then
# uclient-fetch supports -O -, write to stdout
uclient-fetch -O - "$1" 2>/dev/null || return 1
elif command -v wget >/dev/null 2>&1; then
# shellcheck disable=SC2086
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fsSL -k "$1" || return 1
else
curl -fsSL "$1" || return 1
fi
else
return 2
fi
}
fetch() {
# fetch <url> <out>
if command -v uclient-fetch >/dev/null 2>&1; then
uclient-fetch -O "$2" "$1" || return 1
elif command -v wget >/dev/null 2>&1; then
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fSL -k -o "$2" "$1" || return 1
else
curl -fSL -o "$2" "$1" || return 1
fi
else
return 2
fi
# fetch <url> <out>
if command -v uclient-fetch >/dev/null 2>&1; then
uclient-fetch -O "$2" "$1" || return 1
elif command -v wget >/dev/null 2>&1; then
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fSL -k -o "$2" "$1" || return 1
else
curl -fSL -o "$2" "$1" || return 1
fi
else
return 2
fi
}
latest_asset_url() {
# prints URL to stdout, returns 0 on success (public release only)
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
RESP=$(http_get "$API") || return 1
# Extract the first browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
echo "$RESP" |
grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' |
head -n1 |
cut -d '"' -f 4
# prints URL to stdout, returns 0 on success (public release only)
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
RESP=$(http_get "$API") || return 1
# Extract the LAST browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
echo "$RESP" |
grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' |
tail -n1 |
cut -d '"' -f 4
}
main() {
[ -f "$FLAG" ] && return
URL="${WAKEY_TGZ_URL:-}"
if [ -z "$URL" ]; then
HOST=${WAKEY_HOST:-git.ldlda.com}
OWNER=${WAKEY_OWNER:-lda}
REPO=${WAKEY_REPO:-wakey}
if [ -n "${WAKEY_VERSION:-}" ]; then
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
else
URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL"
[ -n "$URL" ] || fail "no asset URL found for arch $ARCH"
fi
fi
[ -f "$FLAG" ] && return
URL="${WAKEY_TGZ_URL:-}"
if [ -z "$URL" ]; then
HOST=${WAKEY_HOST:-git.ldlda.com}
OWNER=${WAKEY_OWNER:-lda}
REPO=${WAKEY_REPO:-wakey}
if [ -n "${WAKEY_VERSION:-}" ]; then
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
else
URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL"
[ -n "$URL" ] || fail "no asset URL found for arch $ARCH"
fi
fi
log "fetching $URL"
if ! fetch "$URL" "$TMPFILE"; then
fail "download failed"
fi
log "fetching $URL"
if ! fetch "$URL" "$TMPFILE"; then
fail "download failed"
fi
log "installing"
mkdir -p "$TMPDIR" "$STAGING"
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
log "installing"
mkdir -p "$TMPDIR" "$STAGING"
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
# Ensure execute bits on staged files we know should be executable
for f in \
"$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/wakey" \
"$STAGING/root/.bin/kill_wakey.sh" \
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
done
# Ensure execute bits on staged files we know should be executable
for f in \
"$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/wakey" \
"$STAGING/root/.bin/kill_wakey.sh" \
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
done
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
for f in \
"$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/"*.sh; do
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
done
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
for f in \
"$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/"*.sh; do
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
done
# Copy staged tree into /
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
# Copy staged tree into /
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
rm -f "$TMPFILE"
rm -rf "$STAGING"
rm -f "$TMPFILE"
rm -rf "$STAGING"
if [ -f /etc/init.d/wakey ]; then
/etc/init.d/wakey enable || true
/etc/init.d/wakey restart || /etc/init.d/wakey start || true
fi
touch "$FLAG"
log "done"
if [ -f /etc/init.d/wakey ]; then
/etc/init.d/wakey enable || true
/etc/init.d/wakey restart || /etc/init.d/wakey start || true
fi
touch "$FLAG"
log "done"
}
main "$@"
+58
View File
@@ -0,0 +1,58 @@
# Shared functions for wakey scripts
function Invoke-Ext {
param($Exe, $Arguments, $Label)
$displayArgs = $Arguments.Clone()
for ($i = 0; $i -lt $displayArgs.Count; $i++) {
if ($displayArgs[$i] -eq '-pw' -and ($i + 1) -lt $displayArgs.Count) {
$displayArgs[$i + 1] = '****'
}
}
Write-Host ("[{0}] {1} {2}" -f $Label, $Exe, ($displayArgs -join ' ')) -ForegroundColor Cyan
$out = & $Exe @Arguments 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error ("{0} failed ({1}):`n{2}" -f $Label, $LASTEXITCODE, ($out -join "`n"))
throw ("{0} failed ({1})" -f $Label, $LASTEXITCODE)
}
return $out
}
function Invoke-Scp {
param($Local, $Dest, $Pass, $HostKey, [switch]$Quiet, [switch]$Recurse)
if ($pscp = Get-Command pscp.exe -ErrorAction SilentlyContinue) {
$arguments = @('-scp')
if ($Quiet) { $arguments += '-q' }
if ($Recurse) { $arguments += '-r' }
if ($HostKey) { $arguments += @('-batch', '-hostkey', $HostKey) }
if ($Pass) { $arguments += @('-pw', $Pass) }
$arguments += @($Local, $Dest)
Invoke-Ext -Exe $pscp.Path -Arguments $arguments -Label 'scp'
}
else {
$arguments = @('-O')
if ($Quiet) { $arguments += '-q' }
if ($Recurse) { $arguments += '-r' }
$arguments += @($Local, $Dest)
Invoke-Ext -Exe 'scp' -Arguments $arguments -Label 'scp'
}
}
function Invoke-Ssh {
param($Cmd, $User, $Remote, $Pass, [switch]$Quiet)
$Cmd = $Cmd -replace "`r`n", "`n" -replace "`r", ""
if ($plink = Get-Command plink.exe -ErrorAction SilentlyContinue) {
$arguments = @('-batch', '-ssh')
if ($Pass) { $arguments += @('-pw', $Pass) }
$dest = if ($User) { "$User@$Remote" } else { $Remote }
$arguments += $dest, $Cmd
Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh'
}
else {
$arguments = @()
if ($Quiet) { $arguments += '-q' }
$dest = if ($User) { "$User@$Remote" } else { $Remote }
$arguments += $dest, $Cmd
Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh'
}
}
+186 -39
View File
@@ -1,22 +1,25 @@
"thanks chatgpt"
import os
import textwrap
from abc import ABC, abstractmethod
from collections.abc import Iterable
from pathlib import Path
from typing import Callable
root = Path(__file__).parent.parent
static = root / "static" # change "assets" to your folder
out_rs = root / "src" / "assets.rs"
static = root / "static"
src = root / "src"
out_rs = src / "assets.rs"
assert 'name = "wakey"' in (root / "Cargo.toml").read_text(encoding="utf-8"), (
"uhh how do i explain this"
)
def sanitize(name: str) -> str:
# Valid Rust identifiers: letters, digits, underscores; no starting digit
out = []
for c in name:
if c.isalnum() or c == "_":
out.append(c)
else:
out.append("_")
s = "".join(out)
s = "".join(c if c.isalnum() else "_" for c in name)
if s and s[0].isdigit():
s = "_" + s
return s
@@ -24,42 +27,186 @@ def sanitize(name: str) -> str:
def indent(text: str, n: int) -> str:
pad = " " * n
return "\n".join(pad + line if line.strip() else line for line in text.splitlines())
return textwrap.indent(text, pad)
class RsAssetFile:
def __init__(self, path: Path):
class RsAsset(ABC):
path: Path
class RsAssetWithName(ABC):
name: str
@staticmethod
@abstractmethod
def name_gen(path: Path) -> str:
"helper"
class RsAssetFile(RsAsset, RsAssetWithName):
__match_args__ = ("path", "name")
def __init__(self, path: Path, name: str | None = None):
self.path = path
if name is None:
self.name: str = RsAssetFile.name_gen(path)
else:
self.name = name
def path_relative_to(self, path: Path):
"helper"
return self.path.relative_to(path, walk_up=True).as_posix()
@staticmethod
def plain_template(const_name: str, relative_path: str):
return f'pub const {const_name}: &str = include_str!("{relative_path}");'
@staticmethod
def macroed_template(const_name: str, relative_path: str):
return f'file {const_name} "{relative_path}"'
@staticmethod
def name_gen(path: Path) -> str:
return sanitize(path.name).upper()
class RsAssetModule(RsAsset, RsAssetWithName):
__match_args__ = ("path", "name")
def __init__(self, path: Path, name: str | None = None):
self.path = path
if name is None:
self.name: str = RsAssetModule.name_gen(path)
else:
self.name = name
@staticmethod
def plain_template(sanitized_name: str, body: str):
"braindead"
indented_body = indent(body, 4)
return f"pub mod {sanitized_name} {{\n{indented_body}\n}}"
@staticmethod
def macroed_template(sanitized_name: str, body: str):
indented_body = indent(body, 4)
return f"folder {sanitized_name} {{\n{indented_body}\n}}"
@staticmethod
def process_body(path: Path, renderer: Callable[[RsAsset], str]):
"here just cuz. Path has to be a folder... so idk"
body = RsAssetModule.iterate_assets(path)
return "\n".join(map(renderer, body))
@staticmethod
def iterate_assets(path: Path) -> "Iterable[RsAssetFile | RsAssetModule]":
subs = []
for f in path.iterdir():
if f.is_file():
yield RsAssetFile(f)
elif f.is_dir():
subs.append(RsAssetModule(f))
yield from subs
@staticmethod
def name_gen(path: Path) -> str:
return sanitize(path.name).lower()
# i meant plain
def render_pain(ass: RsAsset) -> str:
"""
fym i have to deal with all RsAsset subclasses.
ts like a matrix of ahh.
"""
match ass:
case RsAssetRoot(path):
body = RsAssetModule.process_body(path, render_pain)
return RsAssetRoot.plain_template(body)
case RsAssetModule(path, name):
body = RsAssetModule.process_body(path, render_pain)
return RsAssetModule.plain_template(name, body)
case RsAssetFile(path, name) as file:
return RsAssetFile.plain_template(name, file.path_relative_to(src))
case _:
raise TypeError("who are you?")
def render_macro(ass: RsAsset) -> str:
"""
ts is ridiculous
"""
match ass:
case RsAssetRoot(path):
body = RsAssetModule.process_body(path, render_macro)
return RsAssetRoot.macroed_template(body)
case RsAssetModule(path, name):
body = RsAssetModule.process_body(path, render_macro)
return RsAssetModule.macroed_template(name, body)
case RsAssetFile(path, name) as file:
return RsAssetFile.macroed_template(name, file.path_relative_to(src))
case _:
raise TypeError("who are you?")
# region shit ass
# im deleting this code because its so ass
# endregion
lda_macro = """
macro_rules! hehe {
// Folder
(folder $name:ident { $($children:tt)* } $($rest:tt)*) => {
pub mod $name {
hehe!{$($children)*}
}
hehe!{$($rest)*}
};
// File
(file $name:ident $file:literal $($rest:tt)*) => {
pub const $name: &str = include_str!($file);
hehe!{$($rest)*}
};
// Base case
() => {};
}
"""
header = "// generated with ./scripts/map_static.py"
class RsAssetRoot(RsAsset):
__match_args__ = ("path",)
@staticmethod
def plain_template(body: str):
return f"""{header}
{body}
"""
@staticmethod
def macroed_template(body: str):
indented_body = indent(body, 4)
return f"""{header}
{lda_macro}
hehe! {{
{indented_body}
}}
"""
def __init__(self, path: Path) -> None:
os.scandir(path) # is you a dir?
self.path = path
def __str__(self):
const_name = sanitize(self.path.name).upper()
return (
f"pub const {const_name}: &str = "
f'include_str!("{self.path.relative_to(out_rs.parent, walk_up=True).as_posix()}");'
)
# specify walk_up to have .. in yo path
asset_root = RsAssetRoot(static)
class RsAssetModule:
def __init__(self, folder: Path, body_only: bool = False):
self.folder = folder
self.full = not body_only
def __str__(self):
files = [str(RsAssetFile(s)) for s in self.folder.iterdir() if s.is_file()]
subs = [str(RsAssetModule(s)) for s in self.folder.iterdir() if s.is_dir()]
body = "\n".join(files + subs)
return (
f"pub mod {sanitize(self.folder.name).lower()} {{" * self.full
+ f"\n{indent(body, 4*self.full)}\n"
+ self.full * "}"
)
# print(render_pain(asset_root))
# print(render_macro(asset_root))
# rs_code = render_pain(asset_root)
rs_code = render_macro(asset_root)
# generate
rs_code = "// generated with ./scripts/map_static.py\n" + str(
RsAssetModule(static, True)
)
out_rs.write_text(rs_code, encoding="utf-8")
print(f"written to {out_rs}")
print(f"written to {out_rs.relative_to(Path.cwd(), walk_up=True)}")
+6
View File
@@ -52,6 +52,12 @@ if (Test-Path $deploySrc) {
Set-Content -NoNewline -LiteralPath (Join-Path $rootDir "remote_deploy_wakey.sh") -Value $deployContent -Encoding UTF8
}
# Copy static assets
$staticSrc = Join-Path $root "static"
if (Test-Path $staticSrc) {
Copy-Item -Recurse $staticSrc (Join-Path $rootDir "static") -Force
}
# Copy all OpenWrt init scripts present in repo
Get-ChildItem (Join-Path $root 'scripts/init/openwrt') -File | ForEach-Object {
$dest = Join-Path $etcDir $_.Name
+13 -11
View File
@@ -16,6 +16,7 @@ param(
$ErrorActionPreference = 'Stop'
# If no version provided, deduce from Cargo.toml
$versionProvided = [bool]$Version
if (-not $Version) {
$cargoToml = Get-Content Cargo.toml -Raw
if ($cargoToml -match 'version\s*=\s*"([^"]+)"') {
@@ -28,7 +29,8 @@ if (-not $Version) {
}
}
if ($Version) {
# Only write version if it was explicitly provided (not deduced)
if ($versionProvided) {
$pattern = '(?m)^version\s*=\s*"[^"]+"'
$replacement = ('version = "{0}"' -f $Version)
(Get-Content Cargo.toml -Raw) -replace $pattern, $replacement | Set-Content Cargo.toml -Encoding UTF8 -NoNewline
@@ -54,17 +56,17 @@ if ($Publish) {
}
if ($Tag -and $Version) {
# If you git push a tag, this will trigger the act_runner and the release workflow on your self-hosted runner (see .gitea/workflows/release.yml)
$actRunnerScript = Join-Path $PSScriptRoot 'act_runner.ps1'
if (Test-Path $actRunnerScript) {
Write-Host "Ensuring act_runner is running..."
& $actRunnerScript
}
else {
Write-Warning "act_runner.ps1 not found at $actRunnerScript. Skipping runner start."
}
git tag -f "v$Version"
git push -f origin "v$Version"
Write-Host "Pushed tag: v$Version"
Write-Host "Pushed tag: $Version."
# Run act_runner in --once mode to process the release job
$actRunnerScript = Join-Path $PSScriptRoot 'act_runner.ps1'
if (Test-Path $actRunnerScript) {
Write-Host "Starting act_runner (once mode) to process release job..."
& $actRunnerScript -Once
}
else {
Write-Warning "act_runner.ps1 not found at $actRunnerScript. Skipping runner."
}
}
+14 -14
View File
@@ -10,32 +10,32 @@ INIT="/etc/init.d/wakey"
KILL="/root/.bin/kill_wakey.sh"
if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
exit 2
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
exit 2
fi
[ -f "$BIN_TMP" ] || {
echo "tmp binary not found: $BIN_TMP" >&2
exit 1
echo "tmp binary not found: $BIN_TMP" >&2
exit 1
}
chmod +x "$BIN_TMP" || true
if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then
"$INIT" stop || true
elif [ -x "$KILL" ]; then
sh "$KILL" || true
fi
if [ -x "$INIT" ]; then
"$INIT" stop || true
elif [ -x "$KILL" ]; then
sh "$KILL" || true
fi
fi
mv -f "$BIN_TMP" "$DEST"
if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then
"$INIT" start || "$INIT" restart || true
else
nohup "$DEST" >/dev/null 2>&1 &
fi
if [ -x "$INIT" ]; then
"$INIT" start || "$INIT" restart || true
else
nohup "$DEST" >/dev/null 2>&1 &
fi
fi
exit 0
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env pwsh
# Test ipjs on remote ARM device
param(
[string]$Package = "lda-ipjs",
[string]$TestName = "",
[ValidateSet("debug", "release")]
[string]$BuildProfile = "debug",
[string]$password,
[switch]$Verbose,
[string]$RemoteTestPath = "/root/.bin/test",
[string]$RemoteHost = "[email protected]"
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/lib.ps1"
# Build tests and capture output
Write-Host "Building tests for $Package..." -ForegroundColor Cyan
# Stream cargo output anc convert to text
$cargoOutput = cargo test --no-run -p $Package --target armv7-unknown-linux-musleabihf $(if ($BuildProfile -eq "release") { "-r" }) 2>&1 |
ForEach-Object {
$line = if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { $_ }
if ($Verbose) {
Write-Host $line
}
$line # Pass through to capture
}
# Parse test binary paths from cargo output
$testBinaries = $cargoOutput |
Select-String -Pattern "Executable.*\((.+)\)" |
ForEach-Object { $_.Matches.Groups[1].Value } |
Get-Item
if ($testBinaries.Count -eq 0) {
Write-Error "No test binaries found! Exiting..."
# Write-Host "Cargo output:" -ForegroundColor Yellow
# $cargoOutput | ForEach-Object { Write-Host $_ }
exit 1
}
Write-Host "Found $($testBinaries.Count) test $($testBinaries.Count -eq 1 ? "binary" : "binaries")" -ForegroundColor Green
# Run each test binary
foreach ($testBinary in $testBinaries) {
Write-Host "`nTesting: $($testBinary.Name)" -ForegroundColor Cyan
# Copy to target
Invoke-Scp -Local $testBinary.FullName -Dest "${RemoteHost}:$RemoteTestPath" -Pass $password -Quiet
# Run on target
$testArgs = "$(if ($Verbose) {"--nocapture --show-output"})"
if ($TestName) {
$testArgs = "$TestName $testArgs"
}
Invoke-Ssh -Cmd "chmod +x $RemoteTestPath && $RemoteTestPath $testArgs" -Remote $RemoteHost -Pass $password
}
Write-Host "`nDone!" -ForegroundColor Green
+3 -19
View File
@@ -6,25 +6,9 @@ use thiserror::Error;
#[derive(Debug, Display, Error)]
pub enum IPNeighParseError {
IpWhere, // i never seen a ip neigh where the first thing aint an ip
IpParseError(AddrParseError),
IpParseError(#[from] AddrParseError),
// DevWhere,
MacParseError(macaddr::ParseError),
MacParseError(#[from] macaddr::ParseError),
StateWhere, // i never seen a ip neigh without the big FAILED at the end
StateParseError(strum::ParseError),
}
impl From<AddrParseError> for IPNeighParseError {
fn from(value: AddrParseError) -> Self {
Self::IpParseError(value)
}
}
impl From<macaddr::ParseError> for IPNeighParseError {
fn from(value: macaddr::ParseError) -> Self {
Self::MacParseError(value)
}
}
impl From<strum::ParseError> for IPNeighParseError {
fn from(value: strum::ParseError) -> Self {
Self::StateParseError(value)
}
StateParseError(#[from] strum::ParseError),
}
+58 -2
View File
@@ -15,5 +15,61 @@ impl<'de> Deserialize<'de> for NUDState {
}
}
#[allow(unused_imports)]
pub use crate::utils::parse::mac::{des_opm, ser_opm};
use crate::arpparse::IpNeighLine;
use lda_ipjs::subcommands::neighbor::{self as ipjs_neigh, NeighborItem};
impl From<ipjs_neigh::NUDState> for NUDState {
fn from(value: ipjs_neigh::NUDState) -> Self {
match value {
ipjs_neigh::NUDState::Permanent => NUDState::Permanent,
ipjs_neigh::NUDState::Noarp => NUDState::Noarp,
ipjs_neigh::NUDState::Reachable => NUDState::Reachable,
ipjs_neigh::NUDState::Stale => NUDState::Stale,
ipjs_neigh::NUDState::None => NUDState::None,
ipjs_neigh::NUDState::Incomplete => NUDState::Incomplete,
ipjs_neigh::NUDState::Delay => NUDState::Delay,
ipjs_neigh::NUDState::Probe => NUDState::Probe,
ipjs_neigh::NUDState::Failed => NUDState::Failed,
ipjs_neigh::NUDState::Other(_) => NUDState::None,
}
}
}
impl From<NUDState> for ipjs_neigh::NUDState {
fn from(value: NUDState) -> Self {
match value {
NUDState::Permanent => ipjs_neigh::NUDState::Permanent,
NUDState::Noarp => ipjs_neigh::NUDState::Noarp,
NUDState::Reachable => ipjs_neigh::NUDState::Reachable,
NUDState::Stale => ipjs_neigh::NUDState::Stale,
NUDState::None => ipjs_neigh::NUDState::None,
NUDState::Incomplete => ipjs_neigh::NUDState::Incomplete,
NUDState::Delay => ipjs_neigh::NUDState::Delay,
NUDState::Probe => ipjs_neigh::NUDState::Probe,
NUDState::Failed => ipjs_neigh::NUDState::Failed,
}
}
}
impl From<NeighborItem> for IpNeighLine {
fn from(
NeighborItem {
ip,
dev,
mac,
state,
}: NeighborItem,
) -> Self {
IpNeighLine {
ip,
dev,
mac,
state: state
.into_iter()
.map(Into::into)
.max()
.unwrap_or(NUDState::None),
}
}
}
+13 -8
View File
@@ -10,7 +10,7 @@
use std::{net::IpAddr, str::FromStr};
use impls::ser_opm;
use crate::utils::parse::mac;
use macaddr::MacAddr;
use serde_with::skip_serializing_none;
use strum::{Display, EnumString};
@@ -33,7 +33,7 @@ pub struct IpNeighLine {
pub ip: IpAddr,
pub dev: Option<String>,
/// link layer address
#[serde(serialize_with = "ser_opm")]
#[serde(with = "mac::option_mac")]
pub mac: Option<MacAddr>,
/// Neighbour Unreachability Detection
pub state: NUDState,
@@ -41,7 +41,9 @@ pub struct IpNeighLine {
// NUDState custom Deserialize now lives in arpparse/impl.rs; use serde_with OneOrMany for Vec
#[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize)]
#[derive(
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize, Default,
)]
#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
#[serde(rename_all = "UPPERCASE")]
pub enum NUDState {
@@ -63,10 +65,6 @@ pub enum NUDState {
/// neighbour state if it was valid and the
/// address is not changed by this command.
Stale,
/// this is a pseudo state used when initially
/// creating a neighbour entry or after trying to
/// remove it before it becomes free to do so.
None,
/// the neighbour entry has not (yet) been
/// validated/resolved.
@@ -80,6 +78,13 @@ pub enum NUDState {
/// success, neighbor validation has ultimately
/// failed.
Failed,
/// this is a pseudo state used when initially
/// creating a neighbour entry or after trying to
/// remove it before it becomes free to do so.
#[serde(other)]
#[default]
None,
}
impl NUDState {
@@ -178,7 +183,7 @@ impl IpNeighLine {
Self { state, ..self }
}
*/
pub fn with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
pub fn _with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
let dev = dev.into();
move |self_| Self {
dev: Some(dev.clone()),
-12
View File
@@ -1,12 +0,0 @@
// generated with ./scripts/map_static.py
pub const HOME_2_HTML: &str = include_str!("../static/home_2.html");
pub mod home_2 {
pub const DOM_JS: &str = include_str!("../static/home_2/dom.js");
pub const LEASES_JS: &str = include_str!("../static/home_2/leases.js");
pub const MAIN_JS: &str = include_str!("../static/home_2/main.js");
pub const STATUS_JS: &str = include_str!("../static/home_2/status.js");
pub const STYLES_CSS: &str = include_str!("../static/home_2/styles.css");
pub const UTILS_JS: &str = include_str!("../static/home_2/utils.js");
pub const WAKE_JS: &str = include_str!("../static/home_2/wake.js");
}
+4 -7
View File
@@ -2,7 +2,8 @@
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
/// Load MAC->name cache from disk
async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> {
pub(crate) async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>>
{
match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()),
@@ -41,8 +42,8 @@ pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLeaseLine>> {
}
Ok(leases_with_names)
}
use crate::utils::parse::mac;
use macaddr::MacAddr;
use serde::Serializer;
use std::io::{self, ErrorKind};
use std::net::IpAddr;
@@ -52,15 +53,11 @@ pub struct DhcpLeaseLine {
/// Epoch seconds when the lease expires
pub expires_epoch: u64,
pub ip: IpAddr,
#[serde(serialize_with = "ser_mac")]
#[serde(with = "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();
+30 -14
View File
@@ -1,29 +1,44 @@
use axum::{
Router,
routing::{get, post},
};
//! braindead version v0.1.x
//!
//! # whats next
//!
//! for version 2 i hope to have:
//!
//! 1. idk reworked frontend;
//! 2. incorporate ip -j;
//! 3. small 1-5 second caching;
use axum::Router;
use tokio::net::TcpListener;
mod arpparse;
pub mod assets;
mod dhcpparse;
mod route;
mod utils;
use std::io;
const MACHINE_NAME: &str = "lda.lan";
use std::{env, io};
#[cfg(target_os = "linux")]
#[tokio::main]
async fn entry() -> io::Result<()> {
use crate::route::{api_router, home_2, home_2_route, wake_handler};
use crate::route::api_router;
use axum::routing::get_service;
use tower_http::services::ServeDir;
let exe = env::current_exe()?;
let root = exe
.parent()
.ok_or_else(|| io::Error::other("no parent dir"))?;
let static_dir = ServeDir::new(root.join("static"))
.append_index_html_on_directories(true)
.precompressed_br()
.precompressed_deflate()
.precompressed_gzip()
.precompressed_zstd();
let app = Router::new()
// .route("/home", get(home))
.route("/", get(home_2))
.merge(home_2_route())
.route("/wake", post(wake_handler))
// .route("/", get(home_2))
// .merge(home_2_route())
// .route("/status", get(get_status_2))
.nest("/api", api_router());
.nest("/api", api_router())
.fallback_service(get_service(static_dir));
let port = TcpListener::bind("0.0.0.0:12012").await?;
axum::serve(port, app.into_make_service()).await?;
@@ -32,6 +47,7 @@ async fn entry() -> io::Result<()> {
#[cfg(not(target_os = "linux"))]
fn main() -> color_eyre::Result<()> {
use std::net::ToSocketAddrs;
color_eyre::install()?;
// use crate::arpparse::NUDState;
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
-99
View File
@@ -1,99 +0,0 @@
pub mod api;
pub mod devs;
pub mod dhcp;
pub mod status;
pub mod wake;
pub use crate::route::api::{DeviceQuery, api_router};
use crate::{
assets::{self},
utils::{ping::_ping_ip, wake::wake},
};
use axum::{
Router,
http::{StatusCode, header},
response::{Html, IntoResponse},
routing::get,
};
use axum_extra::extract::Query;
use crate::utils::route::serve_js;
use crate::{MACHINE_NAME, utils::_status_build};
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 _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)
}
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 home_2() -> Html<&'static str> {
Html(assets::HOME_2_HTML)
}
pub fn home_2_route() -> Router {
use assets::*;
Router::new()
.route("/home_2", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2/", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2.html", get(|| async { Html(HOME_2_HTML) }))
.route(
"/home_2/styles.css",
get(|| async {
(
[
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
home_2::STYLES_CSS,
)
}),
)
.route("/home_2/main.js", get(|| serve_js(home_2::MAIN_JS)))
.route("/home_2/leases.js", get(|| serve_js(home_2::LEASES_JS)))
.route("/home_2/status.js", get(|| serve_js(home_2::STATUS_JS)))
.route("/home_2/utils.js", get(|| serve_js(home_2::UTILS_JS)))
.route("/home_2/wake.js", get(|| serve_js(home_2::WAKE_JS)))
.route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS)))
}
+50 -24
View File
@@ -1,32 +1,61 @@
use crate::route::error::ApiError;
use crate::utils::query::parser::{QueryType, parse_query};
use axum::Json;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::post;
use axum::{Router, extract::Path, response::Redirect, routing::get};
use axum::{extract::Path, response::Redirect};
use crate::utils::route;
use crate::route::status::{DeviceQuery, Filters, NamePath};
use crate::utils::query::get_ips;
// 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>,
) -> axum::response::Result<Redirect, impl IntoResponse> {
match serde_html_form::to_string(route::status_smart_redirect(q).await) {
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
Err(e) => Err((
StatusCode::BAD_GATEWAY,
Json(StatusError {
error: e.to_string(),
// no less bullshit
let query = match parse_query(q).await {
QueryType::Ip(ip_addr) => DeviceQuery {
filter: Filters {
ips: vec![ip_addr],
..Default::default()
}),
)),
},
..Default::default()
},
QueryType::Mac(mac_addr) => DeviceQuery {
filter: Filters {
macs: vec![mac_addr],
..Default::default()
},
..Default::default()
},
QueryType::Dev(s) => DeviceQuery {
filter: Filters {
devs: vec![s],
..Default::default()
},
..Default::default()
},
QueryType::Nud(nudstate) => DeviceQuery {
filter: Filters {
nuds: vec![nudstate],
..Default::default()
},
..Default::default()
},
QueryType::Unknown(n) => DeviceQuery {
name: Some(n),
..Default::default()
},
};
match serde_html_form::to_string(query) {
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
Err(e) => Err(ApiError {
error: e.to_string(),
code: StatusCode::BAD_GATEWAY,
}),
}
}
pub use crate::route::devs::*;
pub use crate::route::dhcp::*;
pub use crate::route::status::*;
pub use crate::route::wake::*;
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
Redirect::permanent(&format!(
"/api/status?name={name}",
@@ -34,12 +63,9 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
))
}
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))
.route("/wake", post(wake_multi))
pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
get_ips(&name).await.map_or_else(
|e| ApiError::ise(e.to_string()).into_response(),
|ips| Json(ips.collect::<Vec<_>>()).into_response(),
)
}
+1 -1
View File
@@ -2,6 +2,6 @@ use crate::utils::query::dev;
use axum::Json;
pub async fn devs_router() -> Json<Vec<String>> {
dev::devs_sorted().into()
dev::devs_sorted().await.into()
}
// Device listing endpoints
+12 -11
View File
@@ -1,4 +1,8 @@
use crate::{dhcpparse, route::api::StatusError, utils::parse::boolish_str};
use crate::{
dhcpparse::read_dhcp_leases_with_names,
route::error::ApiError,
utils::{parse::boolish_str, query::enrich_leases_with_nud_state},
};
use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
// DHCP lease endpoints
@@ -14,21 +18,18 @@ pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl Into
.map(boolish_str)
.unwrap_or(false);
match dhcpparse::read_dhcp_leases_with_names().await {
match read_dhcp_leases_with_names().await {
Ok(leases_with_names) => {
if !include_state {
return (StatusCode::OK, Json(leases_with_names)).into_response();
}
let out = crate::utils::query::enrich_leases_with_nud_state(leases_with_names).await;
let out = enrich_leases_with_nud_state(leases_with_names).await;
(StatusCode::OK, Json(out)).into_response()
}
Err(e) => (
StatusCode::BAD_GATEWAY,
Json(StatusError {
error: e.to_string(),
..Default::default()
}),
)
.into_response(),
Err(e) => ApiError {
error: e.to_string(),
code: StatusCode::BAD_GATEWAY,
}
.into_response(),
}
}
+29
View File
@@ -0,0 +1,29 @@
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ApiError {
#[serde(skip_serializing, skip_deserializing)]
pub code: StatusCode,
pub error: String,
}
impl ApiError {
/// [StatusCode::INTERNAL_SERVER_ERROR] shortcut
pub const fn ise(error: String) -> Self {
Self {
code: StatusCode::INTERNAL_SERVER_ERROR,
error,
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(self.code, Json(self)).into_response()
}
}
+55
View File
@@ -0,0 +1,55 @@
pub mod api;
pub mod devs;
pub mod dhcp;
pub mod error;
pub mod status;
pub mod wake;
// use crate::assets;
use crate::dhcpparse::load_mac_name_cache;
use crate::route::api::ip;
use crate::route::api::status_redirect;
use crate::route::api::status_smart_redirect;
use crate::route::devs::devs_router;
use crate::route::dhcp::get_dhcp_leases;
use crate::route::status::get_status_json;
use crate::route::wake::wake_multi;
use axum::Json;
use axum::Router;
use axum::body::Body;
use axum::http::Request;
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use std::time::Instant;
async fn add_performance_header(req: Request<Body>, next: Next) -> Response {
let start = Instant::now();
let mut response = next.run(req).await;
let elapsed = start.elapsed();
if let Ok(val) = format!("work-time={}us", elapsed.as_micros()).parse() {
response.headers_mut().insert("Lda-Performance", val);
}
response
}
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))
.route("/wake", post(wake_multi))
.route("/ips/{name}", get(ip))
.route(
"/mac-cache",
get(async || match load_mac_name_cache().await {
Ok(h) => Json(h).into_response(),
Err(e) => e.to_string().into_response(),
}),
)
.layer(middleware::from_fn(add_performance_header))
}
+44 -100
View File
@@ -1,33 +1,20 @@
use crate::{
arpparse::NUDState,
utils::{
parse::{de_many, serialize_macs},
query::get_macs,
},
};
use crate::route::error::ApiError;
use axum::{Json, http::StatusCode, response::IntoResponse};
use axum_extra::extract::Query;
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::collections::HashSet;
use serde_with::{DisplayFromStr, OneOrMany, serde_as};
use std::net::IpAddr;
use crate::arpparse::NUDState;
use crate::utils::query::get_macs;
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
pub struct DeviceQuery {
pub name: Option<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub ip: Vec<IpAddr>,
#[serde(
default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs"
)]
pub mac: Vec<MacAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub dev: Vec<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub nud: Vec<NUDState>,
#[serde(flatten)]
pub filter: Filters,
}
#[derive(Debug, Default, Clone, Hash, Deserialize)]
@@ -43,100 +30,57 @@ pub struct Status<T> {
pub filters: Filters,
}
#[derive(Debug, Default, Serialize)]
#[serde_as]
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct Filters {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ip: Vec<IpAddr>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub dev: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub nud: Vec<NUDState>,
#[serde(
skip_serializing_if = "Vec::is_empty",
serialize_with = "serialize_macs"
)]
pub mac: Vec<MacAddr>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Default)]
pub struct StatusError {
pub name: Option<String>,
pub error: String,
#[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub ips: Vec<IpAddr>,
#[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub devs: Vec<String>,
#[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub nuds: Vec<NUDState>,
#[serde_as(as = "OneOrMany<DisplayFromStr>")]
#[serde(default)]
pub macs: Vec<MacAddr>,
}
pub async fn get_status_json(
Query(DeviceQuery {
name,
ip,
dev,
nud,
mac,
filter: filters,
..
}): 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 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(),
};
let mut tasks = Vec::new();
// can we jus collect the whole table instead of doing this.
for d in &dev_opts {
for n in &nud_opts {
tasks.push(get_macs(
// this i hate sm
name.as_deref(),
ips_opt.as_deref(),
d.as_deref(),
*n,
));
}
}
// why try join all?
match futures::future::try_join_all(tasks)
.await
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
match get_macs(
name.as_slice(),
&filters.ips,
&filters.devs,
&filters.nuds,
&filters.macs,
)
.await
{
Ok(mut table) => {
if !mac.is_empty() {
// mac here
let wanted: HashSet<MacAddr> = mac.into_iter().collect();
table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false));
}
(
StatusCode::OK,
Json(Status {
name,
table,
filters,
}),
)
.into_response()
}
Err(error) => (
StatusCode::BAD_GATEWAY,
Json(StatusError {
Ok(table) => (
StatusCode::OK,
Json(Status {
name,
error: error.to_string(),
table,
filters,
}),
)
.into_response(),
Err(error) => ApiError {
code: StatusCode::BAD_GATEWAY,
error: error
.chain()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(": "),
}
.into_response(),
}
}
// Status endpoints
+15 -21
View File
@@ -4,7 +4,7 @@ use std::net::IpAddr;
/* use crate::arpparse::IpNeighLine;
use crate::route::api::Status; */
use crate::utils::parse::mac::{des_opm, ser_opm};
use crate::utils::parse::mac;
use crate::utils::wake::wake_one;
use axum::{extract::Json, http::StatusCode, response::IntoResponse};
use futures::TryFutureExt;
@@ -23,9 +23,8 @@ pub struct WakeResult {
#[skip_serializing_none]
#[derive(Debug, Serialize, Clone, Copy)]
pub struct WakeTargetResult {
pub ip: Option<IpAddr>,
#[serde(serialize_with = "ser_opm")]
pub mac: Option<MacAddr>,
#[serde(flatten)]
pub target: WakeTarget,
pub status: WakeTargetStatus,
}
@@ -41,11 +40,11 @@ pub enum WakeTargetStatus {
}
#[skip_serializing_none]
#[derive(Debug, Deserialize, Clone, Copy)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct WakeTarget {
#[serde(default)]
pub ip: Option<IpAddr>,
#[serde(default, deserialize_with = "des_opm")]
#[serde(default, with = "mac::option_mac")]
pub mac: Option<MacAddr>,
}
@@ -82,21 +81,15 @@ pub async fn wake_multi_split(
.await?;
sock.set_broadcast(true)?;
Ok(
futures::future::join_all(targets.into_iter().map(async |c| {
if c.is_incomplete() {
c.to_incomplete()
} else {
wake_one(
&sock,
c.try_into().expect("complete struct failed to try_into"), // type state pattern?
)
.await
.into()
}
}))
.await,
)
let iter = targets.into_iter().map(async |c| {
if c.is_incomplete() {
c.to_incomplete()
} else {
let t = c.try_into().expect("complete struct failed to try_into");
wake_one(&sock, t).await.into()
}
});
Ok(futures::future::join_all(iter).await)
}
/* #[derive(Debug, Serialize)]
pub struct WakeStatusLine {
@@ -117,3 +110,4 @@ pub type WakeStatus = Status<WakeStatusLine>; */
// }): Query<DeviceQuery>,
// ) -> impl IntoResponse {
// }
pub mod impls;
@@ -1,4 +1,4 @@
use super::{WakeStatus, WakeTarget, WakeTargetResult};
use crate::utils::wake::{WakeStatus, WakeTarget, WakeTargetResult};
use crate::route::wake::{
WakeTarget as RouteWakeTarget, WakeTargetResult as RouteWakeResult,
WakeTargetStatus as RouteWakeStatus,
@@ -22,25 +22,32 @@ impl TryFrom<RouteWakeTarget> for WakeTarget {
}
}
impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> Self {
impl From<WakeTarget> for RouteWakeTarget {
fn from(WakeTarget { ip, mac }: WakeTarget) -> Self {
Self {
ip: Some(ip),
mac: Some(mac),
}
}
}
impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTargetResult { target, status }: WakeTargetResult) -> Self {
Self {
target: target.into(),
status: status.into(),
}
}
}
impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult {
pub const fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult {
ip: self.ip,
mac: self.mac,
target: self,
status: RouteWakeStatus::Incomplete,
}
}
pub fn is_incomplete(&self) -> bool {
pub const fn is_incomplete(&self) -> bool {
!matches!(
self,
Self {
-64
View File
@@ -1,64 +0,0 @@
// 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 mod wake;
use crate::utils::query::_get_macs_2_1;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
pub mod query;
pub mod route;
// no custom ip deserializer needed when using axum_extra::extract::Query
// but we add a generic one to ignore blanks and accept OneOrMany
pub(crate) mod parse;
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>
"#,
)
}
-10
View File
@@ -1,10 +0,0 @@
use std::io;
pub(crate) async fn exec_command<S: AsRef<std::ffi::OsStr>>(
cmd: S,
args: impl IntoIterator<Item = S>,
) -> io::Result<std::process::Output> {
let mut u = tokio::process::Command::new(cmd);
u.args(args);
u.output().await
}
-56
View File
@@ -1,56 +0,0 @@
use std::{fmt, io};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
/// Failure to resolve a host name to IPs.
DnsResolve { name: String, source: io::Error },
/// External command failed (e.g., ip neigh)
CommandFailed {
cmd: &'static str,
args: Vec<String>,
status: Option<i32>,
stderr: String,
},
/// Generic IO error fallback
Io(io::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::Io(e) => write!(f, "IO error: {e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::DnsResolve { source, .. } => Some(source),
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
+19
View File
@@ -0,0 +1,19 @@
// 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 mod wake;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
pub mod query;
// no custom ip deserializer needed when using axum_extra::extract::Query
// but we add a generic one to ignore blanks and accept OneOrMany
pub(crate) mod parse;
+47
View File
@@ -0,0 +1,47 @@
use macaddr::MacAddr;
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
use serde::{Serialize, Serializer, de};
pub 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)
}
/// Serialize a MacAddr as a string
pub fn serialize<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&mac.to_string())
}
/// Deserialize a MacAddr from a string
pub fn _deserialize<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
where
D: Deserializer<'de>,
{
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
s.parse::<MacAddr>().map_err(DeError::custom)
}
pub mod option_mac {
use super::*;
/// serialize an [`Option<MacAddr>`]
pub fn serialize<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
}
/// deserialize an [`Option<MacAddr>`]
pub fn deserialize<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<&str>::deserialize(des)?
.map(str::parse)
.transpose()
.map_err(de::Error::custom)
}
}
+1 -88
View File
@@ -100,91 +100,4 @@ pub fn boolish_str(s: &str) -> bool {
&& t.parse::<u64>().map(|n| n != 0).unwrap_or(false))
}
pub mod de_many {
use serde::Deserialize;
use serde::de;
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
pub fn vec_from_strs<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let raw: OneOrMany<String> = OneOrMany::<String>::deserialize(des)?;
let mut out = Vec::new();
match raw {
OneOrMany::One(s) => {
let t = s.trim();
if !t.is_empty() {
out.push(t.parse().map_err(de::Error::custom)?);
}
}
OneOrMany::Many(vs) => {
for s in vs {
let t = s.trim();
if t.is_empty() {
continue;
}
out.push(t.parse().map_err(de::Error::custom)?);
}
}
}
Ok(out)
}
}
pub mod mac {
use macaddr::MacAddr;
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
use serde::{Serialize, Serializer, de};
pub 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)
}
/// Serialize a MacAddr as a string
pub fn _serialize_mac<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&mac.to_string())
}
/// Deserialize a MacAddr from a string
pub fn _deserialize_mac<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
where
D: Deserializer<'de>,
{
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
s.parse::<MacAddr>().map_err(DeError::custom)
}
/// serialize an [`Option<MacAddr>`]
pub fn ser_opm<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
}
/// deserialize an [`Option<MacAddr>`]
pub fn des_opm<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<&str>::deserialize(des)?
.map(str::parse)
.transpose()
.map_err(de::Error::custom)
}
}
pub use mac::*;
pub mod mac;
+2 -2
View File
@@ -7,7 +7,7 @@ use tokio::{
time::timeout,
};
use crate::{arpparse::NUDState, utils::query::get_macs};
use crate::{arpparse::NUDState, utils::query::get_mac};
pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
timeout(Duration::from_secs(1), TcpStream::connect(addr))
@@ -19,7 +19,7 @@ pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
}
pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 {
match get_macs(None, Some(&[addr.into()]), None, None).await {
match get_mac(Some(addr.into()), None, &[] as &[NUDState]).await {
Err(_) => 0,
Ok(l) => l
.into_iter()
+52 -32
View File
@@ -1,45 +1,65 @@
use std::{collections::HashSet, fs};
use std::collections::HashSet;
// /// 50ms
// pub async fn get_dev() -> HashSet<String> {
// use lda_ipjs::subcommands::address::json as ipjs_json;
// let mut devs: HashSet<String> = HashSet::new();
// if let Ok(items) = ipjs_json::get(None).await {
// for item in items {
// if item.ifname != "lo" && !item.ifname.is_empty() {
// devs.insert(item.ifname);
// }
// }
// }
// devs
// }
pub fn get_dev() -> HashSet<String> {
let mut devs: HashSet<String> = HashSet::new();
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
for e in rd.flatten() {
if e.file_type()
.map(|ft| {
if ft.is_symlink() {
// true
fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false)
} else {
ft.is_dir()
}
})
.unwrap_or(false)
&& let Ok(name) = e.file_name().into_string()
&& name != "lo"
&& !name.is_empty()
{
devs.insert(name);
/// 3ms
pub async fn get_dev() -> HashSet<String> {
use std::fs;
fn get_dev() -> HashSet<String> {
let mut devs: HashSet<String> = HashSet::new();
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
for e in rd.flatten() {
if e.file_type()
.map(|ft| {
if ft.is_symlink() {
// true
fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false)
} else {
ft.is_dir()
}
})
.unwrap_or(false)
&& 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) {
if let Some((name, _rest)) = line.split_once(':') {
let n = name.trim().to_string();
if n != "lo" && !n.is_empty() {
devs.insert(n);
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
for line in txt.lines().skip(2) {
if let Some((name, _rest)) = line.split_once(':') {
let n = name.trim().to_string();
if n != "lo" && !n.is_empty() {
devs.insert(n);
}
}
}
}
devs
}
devs
tokio::task::spawn_blocking(get_dev)
.await
.unwrap_or_default()
}
pub fn devs_sorted() -> Vec<String> {
let mut v: Vec<String> = get_dev().into_iter().collect();
pub async fn devs_sorted() -> Vec<String> {
let mut v: Vec<String> = get_dev().await.into_iter().collect();
v.sort();
v
}
pub fn has_dev(name: &str) -> bool {
get_dev().contains(name)
pub async fn has_dev(name: &str) -> bool {
get_dev().await.contains(name)
}
+8 -10
View File
@@ -1,5 +1,6 @@
use crate::arpparse::NUDState;
use crate::dhcpparse::DhcpLeaseLine;
use crate::utils::query::get_macs;
use serde_with::skip_serializing_none;
use std::net::IpAddr;
@@ -9,36 +10,33 @@ pub struct DhcpLeaseOut {
#[serde(flatten)]
pub lease_line: DhcpLeaseLine,
pub nud_state: Option<NUDState>,
pub rank: Option<u8>,
}
/// Enrich DHCP leases with NUD state and rank using get_macs
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
use crate::utils::query::macs::get_macs;
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> =
std::collections::HashMap::new();
if let Ok(rows) = get_macs(None, Some(&ips), None, None).await {
let mut map: std::collections::HashMap<IpAddr, NUDState> = std::collections::HashMap::new();
if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await {
for row in rows {
let state = row.state;
let r = state.rank();
map.entry(row.ip)
.and_modify(|e| {
if r > e.1 {
*e = (state, r)
let er = e.rank();
if r > er {
*e = state
}
})
.or_insert((state, r));
.or_insert(state);
}
}
leases
.into_iter()
.map(|lease_line| {
let (nud_state, rank) = map.get(&lease_line.ip).copied().unzip();
let nud_state = map.get(&lease_line.ip).copied();
DhcpLeaseOut {
lease_line,
nud_state,
rank,
}
})
.collect()
+63 -152
View File
@@ -1,173 +1,84 @@
use crate::arpparse::{self, IpNeighLine, NUDState};
use crate::utils::{
cmd::exec_command,
error::{self, Error, Result},
};
use lda_ipjs::subcommands::neighbor;
use macaddr::MacAddr;
use crate::arpparse::{IpNeighLine, NUDState};
use anyhow::{Context, Result};
use std::collections::HashSet;
use std::net::IpAddr;
pub async fn get_ips(machine_name: &str) -> error::Result<impl Iterator<Item = IpAddr>> {
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
Ok(tokio::net::lookup_host((machine_name, 0))
.await
.map_err(|e| error::Error::DnsResolve {
name: machine_name.to_string(),
source: e,
})?
.with_context(|| format!("DNS resolve failed for {machine_name}"))?
.map(|c| c.ip()))
}
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip,
dev: _,
mac,
state,
}| mac.map(|mac| (ip, mac, state)),
)
.collect())
}
pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip: _,
dev: _,
mac,
state: _,
}| mac,
)
.collect())
}
pub async fn get_macs_1(machine_name: &str) -> Result<Vec<arpparse::IpNeighLine>> {
let dev = "br-lan";
let ips = get_ips(machine_name).await?;
let futures = ips.map(|ip| {
let ip = ip.to_canonical();
async move {
let cmd = "ip";
let args = ["neigh", "show", "to", &ip.to_string(), "dev", dev];
let o = exec_command(cmd, args).await?;
if !o.status.success() {
return Err(Error::CommandFailed {
cmd,
args: args.iter().map(ToString::to_string).collect(),
status: o.status.code(),
stderr: String::from_utf8_lossy(&o.stderr).into(),
});
};
Ok(String::from_utf8_lossy(&o.stdout)
.lines()
.flat_map(arpparse::parse_ip_neigh_line)
.collect::<Vec<_>>())
}
});
let res = futures::future::try_join_all(futures).await?;
Ok(res.into_iter().flatten().collect())
}
/// Query neighbor table with multi-filters. Empty slice = no filter.
pub async fn get_macs(
machine_name: Option<&str>,
ips: Option<&[IpAddr]>,
dev: Option<&str>,
state: Option<NUDState>,
machine_names: &[impl AsRef<str>],
ips: &[IpAddr],
devs: &[impl AsRef<str>],
state: &[NUDState],
macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> {
let ip_list: Option<Vec<IpAddr>> =
ips.map(|slice| slice.iter().map(|ip| ip.to_canonical()).collect());
let ip_list = match (ip_list, machine_name) {
(Some(list), _) => list,
(None, Some(name)) => get_ips(name).await?.collect(),
(None, None) => Vec::new(),
};
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, dev, state);
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?;
Ok(res.into_iter().flatten().collect())
// Resolve machine names to IPs
let resolved_ips: HashSet<IpAddr> = if !machine_names.is_empty() {
futures::future::try_join_all(machine_names.iter().map(|n| get_ips(n.as_ref())))
.await?
.into_iter()
.flatten()
.collect()
} else {
run_one(None).await
}
HashSet::new()
};
// Merge provided IPs with resolved IPs
let ip_filter: Vec<IpAddr> = if ips.is_empty() && resolved_ips.is_empty() {
vec![]
} else if ips.is_empty() {
resolved_ips.into_iter().collect()
} else if resolved_ips.is_empty() {
ips.iter().map(|ip| ip.to_canonical()).collect()
} else {
// Intersection: only IPs that appear in both
ips.iter()
.map(|ip| ip.to_canonical())
.filter(|ip| resolved_ips.contains(ip))
.collect()
};
// Convert state filter
let nud_filter: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
// Convert devs to &str for nl::get
let dev_strs: Vec<&str> = devs.iter().map(AsRef::as_ref).collect();
// Single rtnetlink call with all filters
let results: Vec<IpNeighLine> = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
.await
.context("rtnetlink failed")?
.into_iter()
.map(Into::into)
.collect();
Ok(results)
}
/// the atomic get_macs. handle ONE thing only.
/// Legacy single-filter wrapper. Use get_macs for multi-filter.
#[allow(dead_code)]
pub async fn get_mac(
ip: Option<IpAddr>,
dev: Option<&str>,
state: Option<NUDState>,
) -> error::Result<Vec<IpNeighLine>> {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = ip {
args.push("to".into());
args.push(ip.to_string());
}
if let Some(d) = dev {
args.push("dev".into());
args.push(d.to_string());
}
if let Some(nud) = state {
args.push("nud".into());
args.push(nud.as_ip_neigh_arg().into());
}
let cmd = "ip";
let out = exec_command(cmd, args.iter().map(String::as_str).collect::<Vec<_>>()).await?;
if !out.status.success() {
return Err(error::Error::CommandFailed {
cmd,
args,
status: out.status.code(),
stderr: String::from_utf8_lossy(&out.stderr).into(),
});
}
let lines = String::from_utf8_lossy(&out.stdout);
let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line);
let rows: Vec<IpNeighLine> = if let Some(d) = dev {
parsed.map(IpNeighLine::with_dev(d)).collect()
} else {
parsed.collect()
};
Ok(rows)
}
/*
/// get macs where you just run ip neigh then rust handles the filtering (faster than get mac)
pub async fn get_macs_rust(
machine_names: Option<&str>,
ips: Option<&[IpAddr]>,
devs: Option<&[&str]>,
states: Option<&[NUDState]>,
state: &[NUDState],
) -> Result<Vec<IpNeighLine>> {
let mut ip_map: HashSet<IpAddr> = HashSet::new();
let mut machine_map = HashSet::new();
if let Some(ips) = ips {
ip_map.extend(ips);
}
if let Some(m) = machine_names {
machine_map.extend(get_ips(m).await.into_iter().flatten());
}
let ips: Vec<IpAddr> = ip.into_iter().collect();
let devs: Vec<&str> = dev.into_iter().collect();
let nud: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
let real = match (ip_map.is_empty(), machine_map.is_empty()) {
(true, true) => HashSet::new(),
(true, false) => machine_map,
(false, true) => ip_map,
(false, false) => ip_map.intersection(&machine_map).copied().collect(),
};
Ok(vec![])
}
pub async fn _get_machines(m: &[&str]) -> Vec<IpAddr> {
let futs = m.iter().map(|c| get_ips(c));
futures::future::join_all(futs)
Ok(neighbor::nl::get(&ips, &devs, &nud, &[])
.await
.context("rtnetlink failed")?
.into_iter()
.flat_map(|f| f.into_iter().flatten())
.collect()
} */
.map(Into::into)
.collect())
}
@@ -1,6 +1,7 @@
pub mod dev;
pub mod leases;
pub mod macs;
pub mod parser;
pub use leases::*;
pub use macs::*;
+44
View File
@@ -0,0 +1,44 @@
use std::net::IpAddr;
use macaddr::MacAddr;
use crate::{arpparse::NUDState, utils::query::dev::has_dev};
pub enum QueryType {
Ip(IpAddr),
Mac(MacAddr),
Dev(String),
Nud(NUDState),
Unknown(String),
}
pub async fn parse_query(q: String) -> QueryType {
let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q)
} else {
q.trim()
};
// 1) IP
let ip = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok())
} else {
s.parse::<IpAddr>().ok()
};
if let Some(ip) = ip {
return QueryType::Ip(ip);
}
// 2) MAC
if let Ok(mac) = s.parse::<MacAddr>() {
return QueryType::Mac(mac);
}
// 3) NUD state (reachable, stale, ...)
if let Ok(state) = s.parse::<NUDState>() {
return QueryType::Nud(state);
}
// 4) Known device? prefer dev first
if has_dev(s).await {
return QueryType::Dev(s.to_string());
}
// Default: name last // it will fail also
QueryType::Unknown(s.to_string())
}
-72
View File
@@ -1,72 +0,0 @@
use std::net::IpAddr;
use axum::{http::header, response::IntoResponse};
use macaddr::MacAddr;
use crate::{arpparse::NUDState, route::DeviceQuery, utils::query::dev::has_dev};
pub async fn serve_js(content: &'static str) -> impl IntoResponse {
(
[
(header::CONTENT_TYPE, "application/javascript"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
content,
)
}
pub async fn status_smart_redirect(q: String) -> DeviceQuery {
let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q)
} else {
q.trim()
};
// 1) IP
let ip = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok())
} else {
s.parse::<IpAddr>().ok()
};
if let Some(ip) = ip {
let ip = vec![ip];
return DeviceQuery {
ip,
..Default::default()
};
}
// 2) MAC
if let Ok(mac) = s.parse::<MacAddr>() {
let mac = vec![mac];
return DeviceQuery {
mac,
..Default::default()
};
}
// 3) NUD state (reachable, stale, ...)
if let Ok(state) = s.parse::<NUDState>() {
let nud = vec![state];
return DeviceQuery {
nud,
..Default::default()
};
}
// 4) Known device? prefer dev first
if has_dev(s) {
return DeviceQuery {
dev: vec![s.to_string()],
..Default::default()
};
}
// 5) Try DNS: if it resolves, treat as name
/* if tokio::net::lookup_host((s, 0)).await.is_ok() {
return DeviceQuery {
name: Some(s.to_string()),
..Default::default()
};
} */
// Default: name last // it will fail also
DeviceQuery {
name: Some(s.to_string()),
..Default::default()
}
}
+12 -41
View File
@@ -1,39 +1,11 @@
pub mod impls;
//! why did my Head Ass split these into two.
use std::{io, net::IpAddr};
use futures::TryFutureExt;
use macaddr::MacAddr;
use tokio::net::UdpSocket;
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 sent_ok = 0;
for mac in macs {
let mb = mac.as_bytes();
let mut pac = [0; 6 + 6 * 16]; // 6x FF + 16x mac6
pac[..6].fill(0xff);
for i in 1..=16 {
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
}
match suh
.send_to(&pac, (IpAddr::from([192, 168, 100, 255]), 9))
.await
{
Ok(n) if n == pac.len() => sent_ok += 1,
Ok(n) => eprintln!("partial send ({n}/{})", pac.len()),
Err(e) => eprintln!("send error: {e}"),
}
}
Ok(sent_ok)
}
#[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTarget {
pub ip: IpAddr,
@@ -41,8 +13,7 @@ pub struct WakeTarget {
}
#[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTargetResult {
pub ip: IpAddr,
pub mac: MacAddr,
pub target: WakeTarget,
pub status: WakeStatus,
}
#[derive(Debug, Clone, Copy, Hash)]
@@ -52,22 +23,22 @@ pub enum WakeStatus {
WrongSize,
}
impl WakeTarget {
fn _new(ip: IpAddr, mac: MacAddr) -> Self {
const fn _new(ip: IpAddr, mac: MacAddr) -> Self {
Self { ip, mac }
}
fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::Success)
const fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::Success)
}
fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize)
const fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::WrongSize)
}
fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress)
const fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
}
}
impl WakeTargetResult {
fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self {
Self { ip, mac, status }
const fn new(target: WakeTarget, status: WakeStatus) -> Self {
Self { target, status }
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { filter_array } from "./status.js";
// import { filter_array } from "./status.js";
export const qs = new URLSearchParams(location.search);
@@ -21,7 +21,7 @@ export function setPill(kind, text) {
export function setLink(name, clear) {
const url = new URL(location.href);
if (clear) url.searchParams.forEach((_, k) => url.searchParams.delete(k)); // FUCK
if (clear) url.search = ""; // yo
/* const hasExtraFilters = filter_array.some(
(k) => url.searchParams.getAll(k).length
);
+18 -1
View File
@@ -1,5 +1,18 @@
import { elLeases } from "./dom.js";
import { rankState } from "./utils.js";
/**
*
* @param {{
* expires_epoch: Number
* rank?: Number
* ip: String
* mac: String
* nud_state?: String
* name?: String
* }[]} leases
* @returns
*/
export function renderLeases(leases) {
if (!elLeases) return;
if (!Array.isArray(leases) || leases.length === 0) {
@@ -24,7 +37,11 @@ export function renderLeases(leases) {
let dotClass = "dot ok";
if (expired) {
dotClass = "dot bad";
} /* rank from NUDState */ else if (typeof l?.rank === "number") {
} else if (l?.nud_state) {
if (rankState(l.nud_state) >= 5) dotClass = "dot ok";
else if (rankState(l.nud_state) >= 2) dotClass = "dot warn";
else dotClass = "dot bad";
} else if (typeof l?.rank === "number") {
if (l.rank >= 5) dotClass = "dot ok";
else if (l.rank >= 2) dotClass = "dot warn";
else dotClass = "dot bad";
+1
View File
@@ -34,6 +34,7 @@ function updatePreview() {
const raw = getName(elName);
const host = extractHostLikeBackend(raw);
elPreview.textContent = host && host !== raw ? `→ ${host}` : "";
return host;
}
function pickTarget(value) {
+89 -3
View File
@@ -2,6 +2,7 @@ import { elHtml, elLog, setPill, qs, pill } from "./dom.js";
import { rankState } from "./utils.js";
import { merge_wake_data, translate_wake_message } from "./wake.js";
// IpNeighLine
const status_map = {
ip: "ip",
mac: "mac",
@@ -9,8 +10,14 @@ const status_map = {
dev: "interface",
};
export const status_array = Object.keys(status_map);
export const filter_array = ["ip", "dev", "nud", "mac"];
// Filters
export const filter_array = ["ips", "devs", "nuds", "macs"];
/**
*
* @param {String} name
* @returns {URL}
*/
function buildStatusUrl(name) {
const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length);
if (name && !hasExtraFilters) {
@@ -25,6 +32,25 @@ function buildStatusUrl(name) {
return u;
}
/**
*
* @param {{
* has_wake?: true
* table: {
* wake_status?: Boolean
* ips: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* }} data
*/
export function renderStatus(data) {
const tbl = document.createElement("table");
tbl.className = "table";
@@ -82,12 +108,51 @@ export function renderStatus(data) {
if (r >= 5) setPill("ok", "online");
else if (r >= 2) setPill("warn", "maybe");
else setPill("bad", "offline");
if (data.filters.nud?.length > 0) pill.textContent += " (filtered)";
if (data.filters.nuds?.length > 0) pill.textContent += " (filtered)";
} else {
setPill("warn", "unknown");
}
}
/**
* @param {String} name
* @param {Boolean} render
* @param {{
* ip: String,
* mac: String,
* status:
* "incomplete" | "succeed" | "nonexistent_address" | "wrong_size"
* }} data_wake
* @returns {{
* has_wake: false
* table: {
* ip: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* } | {
* has_wake: true
* table: {
* wake_status: Boolean
* ips: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* }}
*/
export async function fetchStatus(name, render = true, data_wake) {
setPill("warn", "checking…");
const u = buildStatusUrl(name);
@@ -97,6 +162,7 @@ export async function fetchStatus(name, render = true, data_wake) {
if (!r.ok) {
let msg = String(r.status);
try {
/** @type {{error: string}} */
const err = await r.clone().json();
msg = err.error || JSON.stringify(err);
} catch {
@@ -106,6 +172,26 @@ export async function fetchStatus(name, render = true, data_wake) {
setPill("bad", "error");
return;
}
/**
* this one does not have data wake
* @type {{
* table: {
* ip: String
* dev: String
* mac: String
* state: String
* wake_status?: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* has_wake?: true
* }}
*/
const data = await r.json();
if (data_wake) {
+7
View File
@@ -15,6 +15,13 @@ body {
padding: 0;
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
@media screen and (max-width: 768px) {
wrap {
margin-inline: 8px;
}
}
body {
display: flex;
min-height: 100dvh;
+31 -3
View File
@@ -1,6 +1,9 @@
import { elHtml, elLog, setPill } from "./dom.js";
import { fetchStatus, renderStatus } from "./status.js";
/**
* @param {String} name just plain name
*/
export async function sendWake(name) {
const data = await fetchStatus(name, false);
if (!data) return; // can not proceed; theres nothing.
@@ -51,8 +54,29 @@ export async function sendWake(name) {
/**
*
* @param {Array} table
* @param {Array} wake
* @param {{
* ip: String
* dev: String
* mac: String
* state: String
* }[]} table
* @param {{
* ip: String,
* mac: String,
* status: "incomplete" | "succeed" | "nonexistent_address" | "wrong_size"
* }[]} wake
* @returns {{
* ip: String
* dev: String
* mac: String
* state: String
* }[] | {
* ip: String
* dev: String
* mac: String
* state: String
* wake_status: boolean
* }[]}
*/
export function merge_wake_data(table, wake) {
if (!wake) return table;
@@ -90,7 +114,11 @@ export function merge_wake_data(table, wake) {
return return_array;
}
/**
*
* @param {"incomplete" | "succeed" | "nonexistent_address" | "wrong_size" | any} wake_msg
* @returns {string}
*/
export function translate_wake_message(wake_msg) {
switch (wake_msg) {
case "incomplete":
+2 -3
View File
@@ -27,11 +27,10 @@
<div class="row muted" style="gap: 16px">
<span
>Uses query
<span title="available keys: name, ip, mac, dev, nud"
<span title="available keys: name, ips, macs, devs, nuds"
>(?name=...)</span
>
on this page to view the status.<!-- and header (X-Target-Name) so either extractor
path works. --></span
on this page to view the status.</span
><span id="preview" class="tiny"></span
><a id="permalink" href="#">permalink</a>
</div>