34 Commits
61 changed files with 1727 additions and 522 deletions

No files matched your search

+7 -2
View File
@@ -1,5 +1,10 @@
[target.armv7-unknown-linux-musleabihf]
linker = "rust-lld"
[build] [build]
target = "armv7-unknown-linux-musleabihf" target = "armv7-unknown-linux-musleabihf"
[target.armv7-unknown-linux-musleabihf] [alias]
linker = "rust-lld" ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
t = "test -- --nocapture --test-threads=1"
tdebug = "test -- --nocapture --test-threads=1 --show-output"
File renamed without changes.
+2
View File
@@ -1,8 +1,10 @@
you should use block code to refence code, like so: you should use block code to refence code, like so:
```rust ```rust
hello_world!(println); hello_world!(println);
// indented lines // indented lines
``` ```
in the chat. Else it fucks up formatting. 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.
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf", "rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf",
"rust-analyzer.diagnostics.disabled": ["unlinked-file"] // "rust-analyzer.diagnostics.disabled": ["unlinked-file"]
} }
Generated
+198 -3
View File
@@ -32,6 +32,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -189,6 +195,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.41" version = "0.4.41"
@@ -470,6 +482,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-range-header"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.1" version = "1.10.1"
@@ -610,6 +628,22 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lda-ipjs"
version = "0.0.1"
dependencies = [
"anyhow",
"futures",
"macaddr",
"rtnetlink",
"serde",
"serde_json",
"serde_with",
"strum",
"thiserror 2.0.16",
"tokio",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.175" version = "0.2.175"
@@ -649,6 +683,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -669,6 +713,66 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "netlink-packet-core"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4"
dependencies = [
"paste",
]
[[package]]
name = "netlink-packet-route"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef"
dependencies = [
"bitflags",
"libc",
"log",
"netlink-packet-core",
]
[[package]]
name = "netlink-proto"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128"
dependencies = [
"bytes",
"futures",
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 2.0.16",
]
[[package]]
name = "netlink-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23"
dependencies = [
"bytes",
"futures",
"libc",
"log",
"tokio",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.1.0" version = "0.1.0"
@@ -705,6 +809,12 @@ version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -767,6 +877,23 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "rtnetlink"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08fd15aa4c64c34d0b3178e45ec6dad313a9f02b193376d501668a7950264bb7"
dependencies = [
"futures",
"log",
"netlink-packet-core",
"netlink-packet-route",
"netlink-proto",
"netlink-sys",
"nix",
"thiserror 1.0.69",
"tokio",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.26" version = "0.1.26"
@@ -999,13 +1126,33 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.16" version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 2.0.16",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@@ -1089,6 +1236,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tokio-util"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@@ -1105,6 +1265,32 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "tower-http"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]] [[package]]
name = "tower-layer" name = "tower-layer"
version = "0.3.3" version = "0.3.3"
@@ -1159,6 +1345,12 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@@ -1179,20 +1371,23 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "wakey" name = "wakey"
version = "0.1.5" version = "0.2.0-alpha"
dependencies = [ dependencies = [
"anyhow",
"axum", "axum",
"axum-extra", "axum-extra",
"color-eyre", "color-eyre",
"futures", "futures",
"lda-ipjs",
"macaddr", "macaddr",
"serde", "serde",
"serde_html_form", "serde_html_form",
"serde_json", "serde_json",
"serde_with", "serde_with",
"strum", "strum",
"thiserror", "thiserror 2.0.16",
"tokio", "tokio",
"tower-http",
"urlencoding", "urlencoding",
] ]
+18 -3
View File
@@ -1,10 +1,11 @@
[package] [package]
name = "wakey" name = "wakey"
version = "0.1.5" version = "0.2.0-alpha"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
[dependencies] [dependencies]
anyhow = "1.0.100"
axum = { version = "0.8.4", features = ["macros"] } axum = { version = "0.8.4", features = ["macros"] }
axum-extra = { version = "0.10.1", features = ["query"] } axum-extra = { version = "0.10.1", features = ["query"] }
color-eyre = "0.6.5" color-eyre = "0.6.5"
@@ -16,7 +17,14 @@ serde_json = "1.0.143"
serde_with = { version = "3.14.0", features = ["json"] } serde_with = { version = "3.14.0", features = ["json"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] } strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
thiserror = "2.0.16" thiserror = "2.0.16"
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] } tokio = { version = "1.47.1", features = [
"fs",
"process",
"rt-multi-thread",
"io-util",
"macros",
] }
tower-http = { version = "0.6.8", features = ["fs"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
[profile.release] [profile.release]
@@ -25,4 +33,11 @@ strip = true
[features] [features]
default = ["very-smart-parsing"] 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"
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "lda-ipjs"
description = "ip -j show schemas"
version = "0.0.1"
edition = "2024"
[dependencies]
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
serde_json = "1.0.143"
serde = { version = "1.0.219", features = ["derive"] }
serde_with = { version = "3.14.0", features = ["json"] }
thiserror = "2.0.16"
anyhow = "1.0.100"
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util", "macros"] }
rtnetlink = "0.18.1"
futures = "0.3.31"
+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
}
+41
View File
@@ -0,0 +1,41 @@
//! ts
//!
//! deals with both ip a (addroutput) and ip l (commonoutput)
//!
//! lowk why its free but its indirection and its ass
pub mod json;
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
}
+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 @@
//! this is purely experimental. im not doing ts no mo
// hallo
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};
// dont you love https://github.com/rust-netlink/rtnetlink/blob/main/examples/get_neighbours.rs
pub async fn get(
ip: Option<IpAddr>,
dev: Option<&str>,
nud: &[NUDState],
) -> anyhow::Result<Vec<NeighborItem>> {
let (gip, gdev, gnud) = (ip, dev, nud);
let (conn, handle, _) = rtnetlink::new_connection()?;
tokio::spawn(conn); // every time?
let mut neighbor_data = handle.neighbours().get().execute();
let nudset: HashSet<&NUDState> = HashSet::from_iter(gnud);
let mut ball: HashMap<u32, String> = HashMap::new();
let mut result = vec![];
'big: while let Some(neighbour_message_item) = neighbor_data.try_next().await? {
// Filter by address family
if !matches!(
neighbour_message_item.header.family,
AddressFamily::Inet | AddressFamily::Inet6
) || matches!(neighbour_message_item.header.state, NeighbourState::Noarp)
// copilot says this to match ip -j n s
{
continue 'big;
}
let state = vec![
neighbour_message_item
.header
.state
.try_into()
.unwrap_or_default(),
]; // ONE ITEM. why tf ts design json.
let mut ip = None;
let mut mac = None;
for neigh_attr in neighbour_message_item.attributes {
match neigh_attr {
NeighbourAttribute::Destination(neighbour_address) => match neighbour_address {
NeighbourAddress::Inet(ipv4_addr) => ip = Some(ipv4_addr.into()),
NeighbourAddress::Inet6(ipv6_addr) => ip = Some(ipv6_addr.into()),
_ => continue 'big,
},
NeighbourAttribute::LinkLocalAddress(items) => {
mac = match items.len() {
6 => items.first_chunk::<6>().map(|&e| MacAddr::from(e)),
8 => items.first_chunk::<8>().map(|&e| MacAddr::from(e)),
_ => continue 'big,
}
}
_ => continue,
}
}
// exquisite
let dev = if let Some(cached) = ball.get(&neighbour_message_item.header.ifindex) {
Some(cached.clone())
} else {
// Query and cache
let name = handle
.link()
.get()
.match_index(neighbour_message_item.header.ifindex)
.execute()
.try_next()
.await?
.and_then(|a| {
a.attributes.into_iter().find_map(|attr| match attr {
LinkAttribute::IfName(name) => Some(name),
_ => None,
})
});
if let Some(ref n) = name {
ball.insert(neighbour_message_item.header.ifindex, n.clone());
}
name
};
let (Some(ip), Some(dev)) = (ip, dev) else {
continue 'big;
};
{
// low block
if let Some(fip) = gip
&& fip != ip
{
continue 'big;
}
if let Some(fdev) = gdev
&& dev != fdev
{
continue 'big;
}
if !nudset.is_empty() && !nudset.contains(&state[0]) {
continue 'big;
};
}
result.push(NeighborItem {
ip,
dev: Some(dev),
mac,
state,
});
}
Ok(result) // now i need another pass to filter out the uh.
}
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), // idk
}
}
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(None, None, &[]).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(None, None, &[]).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(())
}
+60
View File
@@ -0,0 +1,60 @@
# taking back my code AHHHH
this is going to be lengthy. ci is spotless but idk how it works cuz powershell. this is good but i cant afford VPN on the router; we will need to do some bullshit.
## overview
we need a:
- small ahh bin in the router.
- forwards a shell and some functions.
- may be shell calling fns maybe some unsafe c. i cant unsafe c.
- big ahh bin in the vps.
- connect to the small ahh bin thru the wide ass internet.
- forwards some APIs.
- static site
- now need no hosts; apis can be called anywhere anyway.
- do offer these in the big ahh bins tho lets not miss out some shit.
- automation
- gitea
- on the vps or the uh idk.
- ABSOLUTELY can auto build main:latest every day / other day.
- and releases.
- router
- a script to pull the tar. from actions artifacts or from releases. (lua or shell yo)
- a script for the bin and the init.d scripts for these two.
- vps
- a systemd service (ima use ai for ts ngl)
- an auto updater. maybe we overkill and dnf. maybe go easy and git clone + cargo install. maybe we pull from release or nightly. idk
i think idk
## router
### current
everything.
we have some functions that call ip neigh a bajillion times.
we have some other functions that read the same file/folder in /sys or sum shit.
we have a function that broadcast to a set ahh address.
we have some routes; and some data structures acting as schema for them ahh. we dont do posts with json but gets with query strings for some ahh reasons.
these are carryover from the early days.
also from the early days are the js.
static was 100% ai with 0 intervention.
we have a search bar of sort that hooks through a smart endpoint that searches for everything.
we have some bullshit. and two buttons to check and wake. that pings `/api/smart/{query}` and `/wake?name=query`. why.
we have the result of the wake and or status and the errors in the first text box.
the list of cliets with names connected to the router the second box.
thats cool.
### changes?
idk changes.
+19 -2
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( param(
[string]$Pass, [string]$Pass,
[string]$HostName = "192.168.100.1", [string]$HostName = "192.168.100.1",
@@ -47,10 +48,11 @@ sh "`$DEPLOY" $RemoteTmp $RemotePath $RestartFlag
} }
function Invoke-Scp { function Invoke-Scp {
param($Local, $Dest, $Pass, $HostKey, [switch]$Quiet) param($Local, $Dest, $Pass, $HostKey, [switch]$Quiet, [switch]$Recurse)
if ($pscp = Get-Command pscp.exe -ErrorAction SilentlyContinue) { if ($pscp = Get-Command pscp.exe -ErrorAction SilentlyContinue) {
$arguments = @('-scp') $arguments = @('-scp')
if ($Quiet) { $arguments += '-q' } if ($Quiet) { $arguments += '-q' }
if ($Recurse) { $arguments += '-r' }
if ($HostKey) { $arguments += @('-batch', '-hostkey', $HostKey) } if ($HostKey) { $arguments += @('-batch', '-hostkey', $HostKey) }
if ($Pass) { $arguments += @('-pw', $Pass) } if ($Pass) { $arguments += @('-pw', $Pass) }
$arguments += @($Local, $Dest) $arguments += @($Local, $Dest)
@@ -59,6 +61,7 @@ function Invoke-Scp {
else { else {
$arguments = @('-O') $arguments = @('-O')
if ($Quiet) { $arguments += '-q' } if ($Quiet) { $arguments += '-q' }
if ($Recurse) { $arguments += '-r' }
$arguments += @($Local, $Dest) $arguments += @($Local, $Dest)
Invoke-Ext -Exe 'scp' -Arguments $arguments -Label 'scp' Invoke-Ext -Exe 'scp' -Arguments $arguments -Label 'scp'
} }
@@ -102,6 +105,20 @@ try {
# Push main binary # Push main binary
Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Quiet:$Quiet 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 # Push deploy helper if exists
if (Test-Path $localDeploy) { if (Test-Path $localDeploy) {
Invoke-Scp -Local $localDeploy -Dest "$User@${HostName}:$deployTmp" -Pass $Pass -HostKey $HostKey -Quiet:$Quiet Invoke-Scp -Local $localDeploy -Dest "$User@${HostName}:$deployTmp" -Pass $Pass -HostKey $HostKey -Quiet:$Quiet
+5 -5
View File
@@ -10,12 +10,12 @@
START=50 # Run before Tailscale (typically START~80) START=50 # Run before Tailscale (typically START~80)
start() { start() {
LOGFILE=/var/log/update_tailscale.log LOGFILE=/var/log/update_tailscale.log
MAX_RETRIES=10 MAX_RETRIES=10
RETRY_COUNT=0 RETRY_COUNT=0
# shellcheck disable=SC2016 # shellcheck disable=SC2016
sh -c ' sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if fn; then if fn; then
echo "[update_tailscale] Success at $(date)" >>"$LOGFILE" echo "[update_tailscale] Success at $(date)" >>"$LOGFILE"
+7 -7
View File
@@ -8,14 +8,14 @@ NAME=wakey
BIN=/root/.bin/wakey BIN=/root/.bin/wakey
start_service() { start_service() {
procd_open_instance procd_open_instance
procd_set_param command "$BIN" procd_set_param command "$BIN"
procd_set_param respawn 5 1 0 procd_set_param respawn 5 1 0
procd_set_param stdout 1 procd_set_param stdout 1
procd_set_param stderr 1 procd_set_param stderr 1
procd_close_instance procd_close_instance
} }
stop_service() { 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) # 2) Fallback: ps | awk (avoid matching awk/grep themselves)
if [ -z "$pids" ]; then 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 fi
[ -n "$pids" ] || exit 0 [ -n "$pids" ] || exit 0
@@ -18,7 +18,7 @@ kill -TERM "$pids" 2>/dev/null || true
usleep 250000 # uhhh sleep is stupid usleep 250000 # uhhh sleep is stupid
remain="" remain=""
for p in $pids; do for p in $pids; do
kill -0 "$p" 2>/dev/null && remain="$remain $p" kill -0 "$p" 2>/dev/null && remain="$remain $p"
done done
[ -z "$remain" ] || kill -KILL "$remain" 2>/dev/null || true [ -z "$remain" ] || kill -KILL "$remain" 2>/dev/null || true
+8 -8
View File
@@ -3,22 +3,22 @@
set -e set -e
# 0th step from lda is to move the pre assed resolv.conf to the real resolv.conf # 0th step from lda is to move the pre assed resolv.conf to the real resolv.conf
if [ -f /tmp/resolv.conf ]; then 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 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 fi
if [ -x "$(which tailscale)" ]; then if [ -x "$(which tailscale)" ]; then
tailscale update tailscale update
exit 0 exit 0
fi fi
# 1. Fetch the latest version number from Tailscale's site # 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) LATEST=$(curl -s https://pkgs.tailscale.com/stable/ | grep -Eo 'tailscale_[0-9\.]+_arm.tgz' | head -n1)
[ -z "$LATEST" ] && { [ -z "$LATEST" ] && {
echo "❌ Couldn't find latest ARM binary." echo "❌ Couldn't find latest ARM binary."
exit 1 exit 1
} }
URL="https://pkgs.tailscale.com/stable/${LATEST}" URL="https://pkgs.tailscale.com/stable/${LATEST}"
@@ -26,8 +26,8 @@ URL="https://pkgs.tailscale.com/stable/${LATEST}"
cd /var/tmp cd /var/tmp
if [ -f "$LATEST" ]; then if [ -f "$LATEST" ]; then
echo "file exists" echo "file exists"
rm "$LATEST" rm "$LATEST"
fi fi
echo "Downloading $LATEST..." echo "Downloading $LATEST..."
+88 -88
View File
@@ -24,113 +24,113 @@ STAGING="$TMPDIR/wakey-rootfs.$$.$ARCH"
log() { echo "[update_wakey] $*"; } log() { echo "[update_wakey] $*"; }
fail() { fail() {
echo "[update_wakey] ERROR: $*" >&2 echo "[update_wakey] ERROR: $*" >&2
exit 1 exit 1
} }
http_get() { http_get() {
# http_get <url> # http_get <url>
if command -v uclient-fetch >/dev/null 2>&1; then if command -v uclient-fetch >/dev/null 2>&1; then
# uclient-fetch supports -O -, write to stdout # uclient-fetch supports -O -, write to stdout
uclient-fetch -O - "$1" 2>/dev/null || return 1 uclient-fetch -O - "$1" 2>/dev/null || return 1
elif command -v wget >/dev/null 2>&1; then elif command -v wget >/dev/null 2>&1; then
# shellcheck disable=SC2086 # shellcheck disable=SC2086
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1 wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
elif command -v curl >/dev/null 2>&1; then elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fsSL -k "$1" || return 1 curl -fsSL -k "$1" || return 1
else else
curl -fsSL "$1" || return 1 curl -fsSL "$1" || return 1
fi fi
else else
return 2 return 2
fi fi
} }
fetch() { fetch() {
# fetch <url> <out> # fetch <url> <out>
if command -v uclient-fetch >/dev/null 2>&1; then if command -v uclient-fetch >/dev/null 2>&1; then
uclient-fetch -O "$2" "$1" || return 1 uclient-fetch -O "$2" "$1" || return 1
elif command -v wget >/dev/null 2>&1; then elif command -v wget >/dev/null 2>&1; then
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1 wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
elif command -v curl >/dev/null 2>&1; then elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fSL -k -o "$2" "$1" || return 1 curl -fSL -k -o "$2" "$1" || return 1
else else
curl -fSL -o "$2" "$1" || return 1 curl -fSL -o "$2" "$1" || return 1
fi fi
else else
return 2 return 2
fi fi
} }
latest_asset_url() { latest_asset_url() {
# prints URL to stdout, returns 0 on success (public release only) # prints URL to stdout, returns 0 on success (public release only)
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4 HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest" API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
RESP=$(http_get "$API") || return 1 RESP=$(http_get "$API") || return 1
# Extract the first browser_download_url ending with our ARCH .tgz (BusyBox-friendly) # Extract the first browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
echo "$RESP" | echo "$RESP" |
grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' | grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' |
head -n1 | head -n1 |
cut -d '"' -f 4 cut -d '"' -f 4
} }
main() { main() {
[ -f "$FLAG" ] && return [ -f "$FLAG" ] && return
URL="${WAKEY_TGZ_URL:-}" URL="${WAKEY_TGZ_URL:-}"
if [ -z "$URL" ]; then if [ -z "$URL" ]; then
HOST=${WAKEY_HOST:-git.ldlda.com} HOST=${WAKEY_HOST:-git.ldlda.com}
OWNER=${WAKEY_OWNER:-lda} OWNER=${WAKEY_OWNER:-lda}
REPO=${WAKEY_REPO:-wakey} REPO=${WAKEY_REPO:-wakey}
if [ -n "${WAKEY_VERSION:-}" ]; then if [ -n "${WAKEY_VERSION:-}" ]; then
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz" FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE" URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
else else
URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL" 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" [ -n "$URL" ] || fail "no asset URL found for arch $ARCH"
fi fi
fi fi
log "fetching $URL" log "fetching $URL"
if ! fetch "$URL" "$TMPFILE"; then if ! fetch "$URL" "$TMPFILE"; then
fail "download failed" fail "download failed"
fi fi
log "installing" log "installing"
mkdir -p "$TMPDIR" "$STAGING" mkdir -p "$TMPDIR" "$STAGING"
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed" tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
# Ensure execute bits on staged files we know should be executable # Ensure execute bits on staged files we know should be executable
for f in \ for f in \
"$STAGING/etc/init.d/"* \ "$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \ "$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/wakey" \ "$STAGING/root/.bin/wakey" \
"$STAGING/root/.bin/kill_wakey.sh" \ "$STAGING/root/.bin/kill_wakey.sh" \
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do "$STAGING/root/.bin/remote_deploy_wakey.sh"; do
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true [ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
done done
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt) # Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
for f in \ for f in \
"$STAGING/etc/init.d/"* \ "$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \ "$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/"*.sh; do "$STAGING/root/.bin/"*.sh; do
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true [ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
done done
# Copy staged tree into / # Copy staged tree into /
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed" tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
rm -f "$TMPFILE" rm -f "$TMPFILE"
rm -rf "$STAGING" rm -rf "$STAGING"
if [ -f /etc/init.d/wakey ]; then if [ -f /etc/init.d/wakey ]; then
/etc/init.d/wakey enable || true /etc/init.d/wakey enable || true
/etc/init.d/wakey restart || /etc/init.d/wakey start || true /etc/init.d/wakey restart || /etc/init.d/wakey start || true
fi fi
touch "$FLAG" touch "$FLAG"
log "done" log "done"
} }
main "$@" main "$@"
+186 -39
View File
@@ -1,22 +1,25 @@
"thanks chatgpt" "thanks chatgpt"
import os
import textwrap
from abc import ABC, abstractmethod
from collections.abc import Iterable
from pathlib import Path from pathlib import Path
from typing import Callable
root = Path(__file__).parent.parent root = Path(__file__).parent.parent
static = root / "static" # change "assets" to your folder static = root / "static"
out_rs = root / "src" / "assets.rs" 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: def sanitize(name: str) -> str:
# Valid Rust identifiers: letters, digits, underscores; no starting digit # Valid Rust identifiers: letters, digits, underscores; no starting digit
out = [] s = "".join(c if c.isalnum() else "_" for c in name)
for c in name:
if c.isalnum() or c == "_":
out.append(c)
else:
out.append("_")
s = "".join(out)
if s and s[0].isdigit(): if s and s[0].isdigit():
s = "_" + s s = "_" + s
return s return s
@@ -24,42 +27,186 @@ def sanitize(name: str) -> str:
def indent(text: str, n: int) -> str: def indent(text: str, n: int) -> str:
pad = " " * n pad = " " * n
return "\n".join(pad + line if line.strip() else line for line in text.splitlines()) return textwrap.indent(text, pad)
class RsAssetFile: class RsAsset(ABC):
def __init__(self, path: Path): 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 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: # print(render_pain(asset_root))
def __init__(self, folder: Path, body_only: bool = False): # print(render_macro(asset_root))
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 * "}"
)
# rs_code = render_pain(asset_root)
rs_code = render_macro(asset_root)
# generate # generate
rs_code = "// generated with ./scripts/map_static.py\n" + str(
RsAssetModule(static, True)
)
out_rs.write_text(rs_code, encoding="utf-8") 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 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 # Copy all OpenWrt init scripts present in repo
Get-ChildItem (Join-Path $root 'scripts/init/openwrt') -File | ForEach-Object { Get-ChildItem (Join-Path $root 'scripts/init/openwrt') -File | ForEach-Object {
$dest = Join-Path $etcDir $_.Name $dest = Join-Path $etcDir $_.Name
+14 -14
View File
@@ -10,32 +10,32 @@ INIT="/etc/init.d/wakey"
KILL="/root/.bin/kill_wakey.sh" KILL="/root/.bin/kill_wakey.sh"
if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2 echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
exit 2 exit 2
fi fi
[ -f "$BIN_TMP" ] || { [ -f "$BIN_TMP" ] || {
echo "tmp binary not found: $BIN_TMP" >&2 echo "tmp binary not found: $BIN_TMP" >&2
exit 1 exit 1
} }
chmod +x "$BIN_TMP" || true chmod +x "$BIN_TMP" || true
if [ "$RESTART" = "1" ]; then if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then if [ -x "$INIT" ]; then
"$INIT" stop || true "$INIT" stop || true
elif [ -x "$KILL" ]; then elif [ -x "$KILL" ]; then
sh "$KILL" || true sh "$KILL" || true
fi fi
fi fi
mv -f "$BIN_TMP" "$DEST" mv -f "$BIN_TMP" "$DEST"
if [ "$RESTART" = "1" ]; then if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then if [ -x "$INIT" ]; then
"$INIT" start || "$INIT" restart || true "$INIT" start || "$INIT" restart || true
else else
nohup "$DEST" >/dev/null 2>&1 & nohup "$DEST" >/dev/null 2>&1 &
fi fi
fi fi
exit 0 exit 0
+61
View File
@@ -0,0 +1,61 @@
#!/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"
# 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
pscp.exe -batch -scp -pw $password $testBinary.FullName ${RemoteHost}:$RemoteTestPath | Out-Null
# Run on target
$testArgs = "$(if ($Verbose) {"--nocapture --show-output"})"
if ($TestName) {
$testArgs = "$TestName $testArgs"
}
plink -batch -ssh $RemoteHost -pw $password "chmod +x $RemoteTestPath && $RemoteTestPath $testArgs"
}
Write-Host "`nDone!" -ForegroundColor Green
+58 -2
View File
@@ -15,5 +15,61 @@ impl<'de> Deserialize<'de> for NUDState {
} }
} }
#[allow(unused_imports)] use crate::arpparse::IpNeighLine;
pub use crate::utils::parse::mac::{des_opm, ser_opm};
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
.first()
.copied()
.map(Into::into)
.unwrap_or(NUDState::None),
}
}
}
+12 -7
View File
@@ -10,7 +10,7 @@
use std::{net::IpAddr, str::FromStr}; use std::{net::IpAddr, str::FromStr};
use impls::ser_opm; use crate::utils::parse::mac;
use macaddr::MacAddr; use macaddr::MacAddr;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use strum::{Display, EnumString}; use strum::{Display, EnumString};
@@ -33,7 +33,7 @@ pub struct IpNeighLine {
pub ip: IpAddr, pub ip: IpAddr,
pub dev: Option<String>, pub dev: Option<String>,
/// link layer address /// link layer address
#[serde(serialize_with = "ser_opm")] #[serde(with = "mac::option_mac")]
pub mac: Option<MacAddr>, pub mac: Option<MacAddr>,
/// Neighbour Unreachability Detection /// Neighbour Unreachability Detection
pub state: NUDState, 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 // 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)] #[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
#[serde(rename_all = "UPPERCASE")] #[serde(rename_all = "UPPERCASE")]
pub enum NUDState { pub enum NUDState {
@@ -63,10 +65,6 @@ pub enum NUDState {
/// neighbour state if it was valid and the /// neighbour state if it was valid and the
/// address is not changed by this command. /// address is not changed by this command.
Stale, 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 /// the neighbour entry has not (yet) been
/// validated/resolved. /// validated/resolved.
@@ -80,6 +78,13 @@ pub enum NUDState {
/// success, neighbor validation has ultimately /// success, neighbor validation has ultimately
/// failed. /// failed.
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 { impl NUDState {
+28 -9
View File
@@ -1,12 +1,31 @@
// generated with ./scripts/map_static.py // generated with ./scripts/map_static.py
pub const HOME_2_HTML: &str = include_str!("../static/home_2.html"); macro_rules! hehe {
pub mod home_2 { // Folder
pub const DOM_JS: &str = include_str!("../static/home_2/dom.js"); (folder $name:ident { $($children:tt)* } $($rest:tt)*) => {
pub const LEASES_JS: &str = include_str!("../static/home_2/leases.js"); pub mod $name {
pub const MAIN_JS: &str = include_str!("../static/home_2/main.js"); hehe!{$($children)*}
pub const STATUS_JS: &str = include_str!("../static/home_2/status.js"); }
pub const STYLES_CSS: &str = include_str!("../static/home_2/styles.css"); hehe!{$($rest)*}
pub const UTILS_JS: &str = include_str!("../static/home_2/utils.js"); };
pub const WAKE_JS: &str = include_str!("../static/home_2/wake.js"); // File
(file $name:ident $file:literal $($rest:tt)*) => {
pub const $name: &str = include_str!($file);
hehe!{$($rest)*}
};
// Base case
() => {};
}
hehe! {
file HOME_2_HTML "../static/index.html"
folder home_2 {
file DOM_JS "../static/home_2/dom.js"
file LEASES_JS "../static/home_2/leases.js"
file MAIN_JS "../static/home_2/main.js"
file STATUS_JS "../static/home_2/status.js"
file STYLES_CSS "../static/home_2/styles.css"
file UTILS_JS "../static/home_2/utils.js"
file WAKE_JS "../static/home_2/wake.js"
}
} }
+2 -6
View File
@@ -42,8 +42,8 @@ pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLeaseLine>> {
} }
Ok(leases_with_names) Ok(leases_with_names)
} }
use crate::utils::parse::mac;
use macaddr::MacAddr; use macaddr::MacAddr;
use serde::Serializer;
use std::io::{self, ErrorKind}; use std::io::{self, ErrorKind};
use std::net::IpAddr; use std::net::IpAddr;
@@ -53,15 +53,11 @@ pub struct DhcpLeaseLine {
/// Epoch seconds when the lease expires /// Epoch seconds when the lease expires
pub expires_epoch: u64, pub expires_epoch: u64,
pub ip: IpAddr, pub ip: IpAddr,
#[serde(serialize_with = "ser_mac")] #[serde(with = "mac")]
pub mac: MacAddr, pub mac: MacAddr,
pub name: Option<String>, 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 /// Parse one line of /tmp/dhcp.leases
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLeaseLine> { pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLeaseLine> {
let mut c = line.split_whitespace(); let mut c = line.split_whitespace();
+15 -7
View File
@@ -8,26 +8,33 @@
//! 2. incorporate ip -j; //! 2. incorporate ip -j;
//! 3. small 1-5 second caching; //! 3. small 1-5 second caching;
use axum::{Router, routing::get}; use axum::Router;
use tokio::net::TcpListener; use tokio::net::TcpListener;
mod arpparse; mod arpparse;
pub mod assets; pub mod assets;
mod dhcpparse; mod dhcpparse;
mod route; mod route;
mod utils; mod utils;
use std::io; use std::{env, io};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[tokio::main] #[tokio::main]
async fn entry() -> io::Result<()> { async fn entry() -> io::Result<()> {
use crate::route::{api_router, home_2, home_2_route}; 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"));
let app = Router::new() let app = Router::new()
// .route("/home", get(home)) // .route("/home", get(home))
.route("/", get(home_2)) // .route("/", get(home_2))
.merge(home_2_route()) // .merge(home_2_route())
// .route("/status", get(get_status_2)) // .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?; let port = TcpListener::bind("0.0.0.0:12012").await?;
axum::serve(port, app.into_make_service()).await?; axum::serve(port, app.into_make_service()).await?;
@@ -36,6 +43,7 @@ async fn entry() -> io::Result<()> {
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn main() -> color_eyre::Result<()> { fn main() -> color_eyre::Result<()> {
use std::net::ToSocketAddrs;
color_eyre::install()?; color_eyre::install()?;
// use crate::arpparse::NUDState; // use crate::arpparse::NUDState;
// println!("{}", NUDState::Reachable.to_string().to_lowercase()); // println!("{}", NUDState::Reachable.to_string().to_lowercase());
+7 -14
View File
@@ -1,5 +1,5 @@
use crate::route::error::ApiError; use crate::route::error::ApiError;
use crate::utils::query_parser::{QueryType, parse_query}; use crate::utils::query::parser::{QueryType, parse_query};
use axum::Json; use axum::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
@@ -13,7 +13,7 @@ pub async fn status_smart_redirect(
Path(q): Path<String>, Path(q): Path<String>,
) -> axum::response::Result<Redirect, impl IntoResponse> { ) -> axum::response::Result<Redirect, impl IntoResponse> {
// no less bullshit // no less bullshit
let query = match parse_query(q) { let query = match parse_query(q).await {
QueryType::Ip(ip_addr) => DeviceQuery { QueryType::Ip(ip_addr) => DeviceQuery {
filter: Filters { filter: Filters {
ips: vec![ip_addr], ips: vec![ip_addr],
@@ -49,12 +49,10 @@ pub async fn status_smart_redirect(
}; };
match serde_html_form::to_string(query) { match serde_html_form::to_string(query) {
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))), Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
Err(e) => Err(( Err(e) => Err(ApiError {
StatusCode::BAD_GATEWAY, error: e.to_string(),
Json(ApiError { code: StatusCode::BAD_GATEWAY,
error: e.to_string(), }),
}),
)),
} }
} }
@@ -67,12 +65,7 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
pub async fn ip(Path(name): Path<String>) -> impl IntoResponse { pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
get_ips(&name).await.map_or_else( get_ips(&name).await.map_or_else(
|e| { |e| ApiError::ise(e.to_string()).into_response(),
ApiError {
error: e.to_string(),
}
.into_response()
},
|ips| Json(ips.collect::<Vec<_>>()).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; use axum::Json;
pub async fn devs_router() -> Json<Vec<String>> { pub async fn devs_router() -> Json<Vec<String>> {
dev::devs_sorted().into() dev::devs_sorted().await.into()
} }
// Device listing endpoints // Device listing endpoints
+5 -7
View File
@@ -26,12 +26,10 @@ pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl Into
let out = 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() (StatusCode::OK, Json(out)).into_response()
} }
Err(e) => ( Err(e) => ApiError {
StatusCode::BAD_GATEWAY, error: e.to_string(),
Json(ApiError { code: StatusCode::BAD_GATEWAY,
error: e.to_string(), }
}), .into_response(),
)
.into_response(),
} }
} }
+13 -1
View File
@@ -7,11 +7,23 @@ use serde::Serialize;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct ApiError { pub struct ApiError {
#[serde(skip_serializing, skip_deserializing)]
pub code: StatusCode,
pub error: String, 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 { impl IntoResponse for ApiError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response() (self.code, Json(self)).into_response()
} }
} }
+30 -2
View File
@@ -6,6 +6,7 @@ pub mod status;
pub mod wake; pub mod wake;
use crate::assets; use crate::assets;
use crate::dhcpparse::load_mac_name_cache;
use crate::route::api::ip; use crate::route::api::ip;
use crate::route::api::status_redirect; use crate::route::api::status_redirect;
use crate::route::api::status_smart_redirect; use crate::route::api::status_smart_redirect;
@@ -14,8 +15,14 @@ use crate::route::dhcp::get_dhcp_leases;
use crate::route::status::get_status_json; use crate::route::status::get_status_json;
use crate::route::wake::wake_multi; use crate::route::wake::wake_multi;
use axum::routing::post; use axum::Json;
use axum::{Router, http::header, response::Html, routing::get}; use axum::Router;
use axum::body::Body;
use axum::http::{Request, header};
use axum::middleware::{self, Next};
use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post};
use std::time::Instant;
use crate::utils::route::serve_js; use crate::utils::route::serve_js;
@@ -48,6 +55,17 @@ pub fn home_2_route() -> Router {
.route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS))) .route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS)))
} }
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 { pub fn api_router() -> Router {
Router::new() Router::new()
.route("/status/{name}", get(status_redirect)) .route("/status/{name}", get(status_redirect))
@@ -57,4 +75,14 @@ pub fn api_router() -> Router {
.route("/devs", get(devs_router)) .route("/devs", get(devs_router))
.route("/wake", post(wake_multi)) .route("/wake", post(wake_multi))
.route("/ips/{name}", get(ip)) .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))
} }
+31 -51
View File
@@ -4,11 +4,10 @@ use axum_extra::extract::Query;
use macaddr::MacAddr; use macaddr::MacAddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use serde_with::{DisplayFromStr, OneOrMany, serde_as};
use std::net::IpAddr; use std::net::IpAddr;
use crate::arpparse::NUDState; use crate::arpparse::NUDState;
use crate::utils::parse::de_many;
use crate::utils::parse::serialize_macs;
use crate::utils::query::get_macs; use crate::utils::query::get_macs;
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
@@ -31,76 +30,57 @@ pub struct Status<T> {
pub filters: Filters, pub filters: Filters,
} }
#[serde_as]
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct Filters { pub struct Filters {
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub ips: Vec<IpAddr>, pub ips: Vec<IpAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub devs: Vec<String>, pub devs: Vec<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub nuds: Vec<NUDState>, pub nuds: Vec<NUDState>,
#[serde( #[serde_as(as = "OneOrMany<DisplayFromStr>")]
default, #[serde(default)]
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs"
)]
pub macs: Vec<MacAddr>, pub macs: Vec<MacAddr>,
} }
#[deprecated = "use ApiError"]
#[skip_serializing_none]
#[derive(Debug, Serialize, Default)]
pub struct StatusError {
pub name: Option<String>,
pub error: String,
}
pub async fn get_status_json( pub async fn get_status_json(
Query(DeviceQuery { Query(DeviceQuery {
name, name,
filter: filter: filters,
Filters {
ips,
devs,
nuds,
macs,
},
.. ..
}): Query<DeviceQuery>, }): Query<DeviceQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match get_macs( match get_macs(
&name.iter().collect::<Vec<_>>(), name.as_slice(),
&ips, &filters.ips,
&devs.iter().collect::<Vec<_>>(), &filters.devs,
&nuds, &filters.nuds,
&macs, &filters.macs,
) )
.await .await
{ {
Ok(table) => { Ok(table) => (
let filters = Filters { StatusCode::OK,
ips, Json(Status {
devs, name,
nuds, table,
macs, filters,
};
(
StatusCode::OK,
Json(Status {
name,
table,
filters,
}),
)
.into_response()
}
Err(error) => (
StatusCode::BAD_GATEWAY,
Json(ApiError {
error: error.to_string(),
}), }),
) )
.into_response(), .into_response(),
Err(error) => ApiError {
code: StatusCode::BAD_GATEWAY,
error: error
.chain()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(": "),
}
.into_response(),
} }
} }
// Status endpoints // Status endpoints
+2 -2
View File
@@ -4,7 +4,7 @@ use std::net::IpAddr;
/* use crate::arpparse::IpNeighLine; /* use crate::arpparse::IpNeighLine;
use crate::route::api::Status; */ 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 crate::utils::wake::wake_one;
use axum::{extract::Json, http::StatusCode, response::IntoResponse}; use axum::{extract::Json, http::StatusCode, response::IntoResponse};
use futures::TryFutureExt; use futures::TryFutureExt;
@@ -44,7 +44,7 @@ pub enum WakeTargetStatus {
pub struct WakeTarget { pub struct WakeTarget {
#[serde(default)] #[serde(default)]
pub ip: Option<IpAddr>, pub ip: Option<IpAddr>,
#[serde(default, serialize_with = "ser_opm", deserialize_with = "des_opm")] #[serde(default, with = "mac::option_mac")]
pub mac: Option<MacAddr>, pub mac: Option<MacAddr>,
} }
-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)
}
}
-3
View File
@@ -9,13 +9,10 @@
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from)); // pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub mod wake; pub mod wake;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input /// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad // this is so bad
pub mod ping; pub mod ping;
pub mod query; pub mod query;
pub mod query_parser;
pub mod route; pub mod route;
// no custom ip deserializer needed when using axum_extra::extract::Query // no custom ip deserializer needed when using axum_extra::extract::Query
+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)) && t.parse::<u64>().map(|n| n != 0).unwrap_or(false))
} }
pub mod de_many { pub mod mac;
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::*;
+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> { /// 3ms
let mut devs: HashSet<String> = HashSet::new(); pub async fn get_dev() -> HashSet<String> {
if let Ok(rd) = std::fs::read_dir("/sys/class/net") { use std::fs;
for e in rd.flatten() { fn get_dev() -> HashSet<String> {
if e.file_type() let mut devs: HashSet<String> = HashSet::new();
.map(|ft| { if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
if ft.is_symlink() { for e in rd.flatten() {
// true if e.file_type()
fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false) .map(|ft| {
} else { if ft.is_symlink() {
ft.is_dir() // true
} fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false)
}) } else {
.unwrap_or(false) ft.is_dir()
&& let Ok(name) = e.file_name().into_string() }
&& name != "lo" })
&& !name.is_empty() .unwrap_or(false)
{ && let Ok(name) = e.file_name().into_string()
devs.insert(name); && name != "lo"
&& !name.is_empty()
{
devs.insert(name);
}
} }
} } else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") { for line in txt.lines().skip(2) {
for line in txt.lines().skip(2) { if let Some((name, _rest)) = line.split_once(':') {
if let Some((name, _rest)) = line.split_once(':') { let n = name.trim().to_string();
let n = name.trim().to_string(); if n != "lo" && !n.is_empty() {
if n != "lo" && !n.is_empty() { devs.insert(n);
devs.insert(n); }
} }
} }
} }
devs
} }
devs tokio::task::spawn_blocking(get_dev)
.await
.unwrap_or_default()
} }
pub fn devs_sorted() -> Vec<String> { pub async fn devs_sorted() -> Vec<String> {
let mut v: Vec<String> = get_dev().into_iter().collect(); let mut v: Vec<String> = get_dev().await.into_iter().collect();
v.sort(); v.sort();
v v
} }
pub fn has_dev(name: &str) -> bool { pub async fn has_dev(name: &str) -> bool {
get_dev().contains(name) get_dev().await.contains(name)
} }
+6 -8
View File
@@ -10,35 +10,33 @@ pub struct DhcpLeaseOut {
#[serde(flatten)] #[serde(flatten)]
pub lease_line: DhcpLeaseLine, pub lease_line: DhcpLeaseLine,
pub nud_state: Option<NUDState>, pub nud_state: Option<NUDState>,
pub rank: Option<u8>,
} }
/// Enrich DHCP leases with NUD state and rank using get_macs /// Enrich DHCP leases with NUD state and rank using get_macs
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> { pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect(); let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> = let mut map: std::collections::HashMap<IpAddr, NUDState> = std::collections::HashMap::new();
std::collections::HashMap::new();
if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await { if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await {
for row in rows { for row in rows {
let state = row.state; let state = row.state;
let r = state.rank(); let r = state.rank();
map.entry(row.ip) map.entry(row.ip)
.and_modify(|e| { .and_modify(|e| {
if r > e.1 { let er = e.rank();
*e = (state, r) if r > er {
*e = state
} }
}) })
.or_insert((state, r)); .or_insert(state);
} }
} }
leases leases
.into_iter() .into_iter()
.map(|lease_line| { .map(|lease_line| {
let (nud_state, rank) = map.get(&lease_line.ip).copied().unzip(); let nud_state = map.get(&lease_line.ip).copied();
DhcpLeaseOut { DhcpLeaseOut {
lease_line, lease_line,
nud_state, nud_state,
rank,
} }
}) })
.collect() .collect()
+32 -29
View File
@@ -1,28 +1,17 @@
use macaddr::MacAddr; use macaddr::MacAddr;
use crate::arpparse::{self, IpNeighLine, NUDState}; use crate::arpparse::{self, IpNeighLine, NUDState};
use crate::utils::{ use anyhow::{Context, Result, bail};
cmd::exec_command,
error::{self, Result},
};
use std::collections::HashSet; use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
pub async fn get_ips(machine_name: &str) -> 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)) Ok(tokio::net::lookup_host((machine_name, 0))
.await .await
.map_err(|e| error::Error::DnsResolve { .with_context(|| format!("DNS resolve failed for {machine_name}"))?
name: machine_name.to_string(),
source: e,
})?
.map(|c| c.ip())) .map(|c| c.ip()))
} }
// #[deprecated(
// since = "0.1.5",
// note = "just call once everything with get mac
// and then filter it bro WHY DO YOU EVEN DO TS"
// )]
// good now // good now
// //
// Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field. // Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field.
@@ -45,15 +34,12 @@ pub async fn get_macs(
macs: &[MacAddr], macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect(); let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect();
let ip_m: HashSet<IpAddr> = futures::future::try_join_all( let ip_m: HashSet<IpAddr> =
machine_names futures::future::try_join_all(machine_names.iter().map(|c| get_ips(c.as_ref())))
.iter() .await?
.map(|c| async { get_ips(c.as_ref()).await }), .into_iter()
) .flatten()
.await? .collect();
.into_iter()
.flatten()
.collect();
let ip_all = if ip_set.is_empty() && ip_m.is_empty() { let ip_all = if ip_set.is_empty() && ip_m.is_empty() {
None None
} else if ip_set.is_empty() { } else if ip_set.is_empty() {
@@ -108,6 +94,24 @@ pub async fn get_macs(
Ok(ip_filtered) Ok(ip_filtered)
} }
// /// the atomic get_macs. handle ONE thing only.
// pub async fn get_mac(
// ip: Option<IpAddr>,
// dev: Option<&str>,
// state: &[NUDState],
// ) -> Result<Vec<IpNeighLine>> {
// use lda_ipjs::subcommands::neighbor as ipjs_neigh;
// let ipjs_states: Vec<ipjs_neigh::NUDState> = state.iter().copied().map(Into::into).collect();
// let items = ipjs_neigh::json::get(ip, dev, &ipjs_states)
// .await
// .context("Calling ip -j neigh failed")?;
// let lines = items.into_iter().map(Into::into).collect();
// Ok(lines)
// }
/// the atomic get_macs. handle ONE thing only. /// the atomic get_macs. handle ONE thing only.
pub async fn get_mac( pub async fn get_mac(
ip: Option<IpAddr>, ip: Option<IpAddr>,
@@ -128,15 +132,14 @@ pub async fn get_mac(
args.push(nud.as_ip_neigh_arg().into()); args.push(nud.as_ip_neigh_arg().into());
} }
let cmd = "ip"; let cmd = "ip";
let out = exec_command(cmd, args.iter().map(String::as_str).collect::<Vec<_>>()).await?; let mut u = tokio::process::Command::new(cmd);
u.args(args);
let out = u.output().await?;
if !out.status.success() { if !out.status.success() {
return Err(error::Error::CommandFailed { bail!(String::from_utf8_lossy(&out.stderr).into_owned());
cmd,
args,
status: out.status.code(),
stderr: String::from_utf8_lossy(&out.stderr).into(),
});
} }
let lines = String::from_utf8_lossy(&out.stdout); let lines = String::from_utf8_lossy(&out.stdout);
let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line); let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line);
let rows: Vec<IpNeighLine> = if let Some(d) = dev { let rows: Vec<IpNeighLine> = if let Some(d) = dev {
+1
View File
@@ -1,6 +1,7 @@
pub mod dev; pub mod dev;
pub mod leases; pub mod leases;
pub mod macs; pub mod macs;
pub mod parser;
pub use leases::*; pub use leases::*;
pub use macs::*; pub use macs::*;
@@ -12,7 +12,7 @@ pub enum QueryType {
Unknown(String), Unknown(String),
} }
pub fn parse_query(q: String) -> QueryType { pub async fn parse_query(q: String) -> QueryType {
let s = if cfg!(feature = "very-smart-parsing") { let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q) crate::utils::parse::extract_host(&q)
} else { } else {
@@ -36,7 +36,7 @@ pub fn parse_query(q: String) -> QueryType {
return QueryType::Nud(state); return QueryType::Nud(state);
} }
// 4) Known device? prefer dev first // 4) Known device? prefer dev first
if has_dev(s) { if has_dev(s).await {
return QueryType::Dev(s.to_string()); return QueryType::Dev(s.to_string());
} }
// Default: name last // it will fail also // Default: name last // it will fail also
+2 -2
View File
@@ -41,13 +41,13 @@ impl From<WakeTargetResult> for RouteWakeResult {
} }
impl RouteWakeTarget { impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult { pub const fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult { RouteWakeResult {
target: self, target: self,
status: RouteWakeStatus::Incomplete, status: RouteWakeStatus::Incomplete,
} }
} }
pub fn is_incomplete(&self) -> bool { pub const fn is_incomplete(&self) -> bool {
!matches!( !matches!(
self, self,
Self { Self {
+7 -5
View File
@@ -1,3 +1,5 @@
//! why did my Head Ass split these into two.
pub mod impls; pub mod impls;
use std::{io, net::IpAddr}; use std::{io, net::IpAddr};
@@ -22,21 +24,21 @@ pub enum WakeStatus {
WrongSize, WrongSize,
} }
impl WakeTarget { impl WakeTarget {
fn _new(ip: IpAddr, mac: MacAddr) -> Self { const fn _new(ip: IpAddr, mac: MacAddr) -> Self {
Self { ip, mac } Self { ip, mac }
} }
fn good(self) -> WakeTargetResult { const fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::Success) WakeTargetResult::new(self, WakeStatus::Success)
} }
fn bad(self) -> WakeTargetResult { const fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::WrongSize) WakeTargetResult::new(self, WakeStatus::WrongSize)
} }
fn errored(self) -> WakeTargetResult { const fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::NonexistentAddress) WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
} }
} }
impl WakeTargetResult { impl WakeTargetResult {
fn new(target: WakeTarget, status: WakeStatus) -> Self { const fn new(target: WakeTarget, status: WakeStatus) -> Self {
Self { target, status } Self { target, status }
} }
} }
+1 -1
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); export const qs = new URLSearchParams(location.search);
+6 -1
View File
@@ -1,4 +1,5 @@
import { elLeases } from "./dom.js"; import { elLeases } from "./dom.js";
import { rankState } from "./utils.js";
/** /**
* *
@@ -36,7 +37,11 @@ export function renderLeases(leases) {
let dotClass = "dot ok"; let dotClass = "dot ok";
if (expired) { if (expired) {
dotClass = "dot bad"; 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"; if (l.rank >= 5) dotClass = "dot ok";
else if (l.rank >= 2) dotClass = "dot warn"; else if (l.rank >= 2) dotClass = "dot warn";
else dotClass = "dot bad"; else dotClass = "dot bad";
+1
View File
@@ -34,6 +34,7 @@ function updatePreview() {
const raw = getName(elName); const raw = getName(elName);
const host = extractHostLikeBackend(raw); const host = extractHostLikeBackend(raw);
elPreview.textContent = host && host !== raw ? `→ ${host}` : ""; elPreview.textContent = host && host !== raw ? `→ ${host}` : "";
return host;
} }
function pickTarget(value) { function pickTarget(value) {
+1 -2
View File
@@ -30,8 +30,7 @@
<span title="available keys: name, ips, macs, devs, nuds" <span title="available keys: name, ips, macs, devs, nuds"
>(?name=...)</span >(?name=...)</span
> >
on this page to view the status.<!-- and header (X-Target-Name) so either extractor on this page to view the status.</span
path works. --></span
><span id="preview" class="tiny"></span ><span id="preview" class="tiny"></span
><a id="permalink" href="#">permalink</a> ><a id="permalink" href="#">permalink</a>
</div> </div>