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] [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"
+3 -1
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.
+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] [package]
name = "wakey" name = "wakey"
version = "0.1.4" version = "0.1.7"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
[dependencies] [dependencies]
axum = { version = "0.8.4", features = ["macros"] } anyhow = "1"
axum-extra = { version = "0.10.1", features = ["query"] } axum = { version = "0", features = ["macros"] }
color-eyre = "0.6.5" axum-extra = { version = "0", features = ["query"] }
futures = "0.3.31" color-eyre = "0"
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] } futures = "0"
serde = { version = "1.0.219", features = ["derive"] } macaddr = { version = "1", features = ["serde", "serde_std"] }
serde_html_form = "0.2.7" serde = { version = "1", features = ["derive"] }
serde_json = "1.0.143" serde_html_form = "0"
serde_with = { version = "3.14.0", features = ["json"] } serde_json = "1"
strum = { version = "0.27.2", features = ["derive", "strum_macros"] } serde_with = { version = "3", features = ["json"] }
thiserror = "2.0.16" strum = { version = "0", features = ["derive", "strum_macros"] }
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] } thiserror = "2"
urlencoding = "2.1.3" tokio = { version = "1", features = [
"fs",
"process",
"rt-multi-thread",
"io-util",
"macros",
] }
tower-http = { version = "0", features = ["fs"] }
urlencoding = "2"
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"
@@ -25,4 +33,13 @@ 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"
registry = "gitea"
version = "*"
+10 -13
View File
@@ -2,19 +2,16 @@
router shit router shit
## usage ## whatever i did must be BEAUTIFUL
- Build (Windows host OK): read [in vietnamese](https://ldlda.com/blogs/else/ssh-ax3000cv2-update-firmware-0-10-2)
- 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
Optional updater (fetch latest automatically on OpenWrt): ## now i NEED the split arch to be on
```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)
WAKEY_HOST=git.ldlda.com WAKEY_OWNER=lda WAKEY_REPO=wakey sh /etc/ldlda_help/update_wakey.sh
``` 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. # Opt-in to non-interactive configure; by default we print the command for you to run manually.
[switch]$ForceConfigure, [switch]$ForceConfigure,
# When -Attach, run in the foreground to see logs (good for first-time troubleshooting) # 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' $ErrorActionPreference = 'Stop'
@@ -50,11 +52,13 @@ if (-not (Test-Path $configFile)) {
} }
} }
if ($Attach) { if ($Attach -or $Once) {
Write-Host "Starting act_runner (attached)... Ctrl+C to stop" Write-Host "Starting act_runner $(if ($Once) {'(once mode)'} else {'(attached)'})... $(if (-not $Once) {'Ctrl+C to stop'})"
$configParent = Split-Path -Parent $configFile $configParent = Split-Path -Parent $configFile
Push-Location $configParent Push-Location $configParent
& $RunnerPath daemon --config $configFile $daemonArgs = @('daemon', '--config', $configFile)
if ($Once) { $daemonArgs += '--once' }
& $RunnerPath @daemonArgs
Pop-Location Pop-Location
} }
else { 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( param(
[string]$Pass, [string]$Pass,
[string]$HostName = "192.168.100.1", [string]$HostName = "192.168.100.1",
@@ -14,23 +15,7 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
try { $PSStyle.OutputRendering = 'Host' } catch {} try { $PSStyle.OutputRendering = 'Host' } catch {}
function Invoke-Ext { . "$PSScriptRoot/lib.ps1"
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 Get-DeployScript { function Get-DeployScript {
param($DeployPreferred, $DeployTmp, $RemoteTmp, $RemotePath, $RestartFlag) param($DeployPreferred, $DeployTmp, $RemoteTmp, $RemotePath, $RestartFlag)
@@ -42,43 +27,7 @@ if [ ! -x "`$DEPLOY" ] && [ -f $DeployTmp ]; then
DEPLOY=$DeployTmp DEPLOY=$DeployTmp
fi fi
sh "`$DEPLOY" $RemoteTmp $RemotePath $RestartFlag 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 ---- #---- Main Flow ----
@@ -102,6 +51,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
+18 -19
View File
@@ -9,23 +9,22 @@
START=50 # Run before Tailscale (typically START~80) START=50 # Run before Tailscale (typically START~80)
start() { do_update() {
LOGFILE=/var/log/update_tailscale.log MAX_RETRIES=10
MAX_RETRIES=10 RETRY_COUNT=0
RETRY_COUNT=0 while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if /etc/ldlda_help/update_tailscale.sh; then
# shellcheck disable=SC2016 echo "[update_tailscale] Success at $(date)"
nohup sh -c ' return 0
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do fi
if fn; then RETRY_COUNT=$((RETRY_COUNT + 1))
echo "[update_tailscale] Success at $(date)" >>"$LOGFILE" echo "[update_tailscale] Retry $RETRY_COUNT at $(date)"
exit 0 sleep 5
fi done
RETRY_COUNT=$((RETRY_COUNT + 1)) echo "[update_tailscale] Too many retries. Exiting at $(date)"
echo "[update_tailscale] Retry $RETRY_COUNT at $(date)" >>"$LOGFILE" return 1
sleep 5 }
done
echo "[update_tailscale] Too many retries. Exiting at $(date)" >>"$LOGFILE" start() {
exit 1 do_update >> /var/log/update_tailscale.log 2>&1 &
' >>"$LOGFILE" 2>&1 &
} }
+18 -17
View File
@@ -1,23 +1,24 @@
#!/bin/sh /etc/rc.common #!/bin/sh /etc/rc.common
START=66 START=66
start() { # does this work now fam
LOGFILE=/var/log/update_wakey.log # exposal to wg is separate; so weon care
do_update() {
MAX_RETRIES=10 MAX_RETRIES=10
RETRY_COUNT=0 RETRY_COUNT=0
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
# shellcheck disable=SC2016 if /etc/ldlda_help/update_wakey.sh; then
sh -c ' echo "[update_wakey] Success at $(date)"
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do return 0
if /etc/ldlda_help/update_wakey.sh; then fi
echo "[update_wakey] Success at $(date)" >>"$LOGFILE" RETRY_COUNT=$((RETRY_COUNT + 1))
exit 0 echo "[update_wakey] Retry $RETRY_COUNT at $(date)"
fi sleep 5
RETRY_COUNT=$((RETRY_COUNT + 1)) done
echo "[update_wakey] Retry $RETRY_COUNT at $(date)" >>"$LOGFILE" echo "[update_wakey] Too many retries. Exiting at $(date)"
sleep 5 return 1
done }
echo "[update_wakey] Too many retries. Exiting at $(date)" >>"$LOGFILE"
exit 1 start() {
' >>"$LOGFILE" 2>&1 & do_update >>/var/log/update_wakey.log 2>&1 &
} }
+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 LAST 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 | tail -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 "$@"
+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" "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
+13 -11
View File
@@ -16,6 +16,7 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
# If no version provided, deduce from Cargo.toml # If no version provided, deduce from Cargo.toml
$versionProvided = [bool]$Version
if (-not $Version) { if (-not $Version) {
$cargoToml = Get-Content Cargo.toml -Raw $cargoToml = Get-Content Cargo.toml -Raw
if ($cargoToml -match 'version\s*=\s*"([^"]+)"') { 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*"[^"]+"' $pattern = '(?m)^version\s*=\s*"[^"]+"'
$replacement = ('version = "{0}"' -f $Version) $replacement = ('version = "{0}"' -f $Version)
(Get-Content Cargo.toml -Raw) -replace $pattern, $replacement | Set-Content Cargo.toml -Encoding UTF8 -NoNewline (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 ($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 tag -f "v$Version"
git push -f origin "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" 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
+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)] #[derive(Debug, Display, Error)]
pub enum IPNeighParseError { pub enum IPNeighParseError {
IpWhere, // i never seen a ip neigh where the first thing aint an ip IpWhere, // i never seen a ip neigh where the first thing aint an ip
IpParseError(AddrParseError), IpParseError(#[from] AddrParseError),
// DevWhere, // DevWhere,
MacParseError(macaddr::ParseError), MacParseError(#[from] macaddr::ParseError),
StateWhere, // i never seen a ip neigh without the big FAILED at the end StateWhere, // i never seen a ip neigh without the big FAILED at the end
StateParseError(strum::ParseError), StateParseError(#[from] 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)
}
} }
+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
.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 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 {
@@ -178,7 +183,7 @@ impl IpNeighLine {
Self { state, ..self } 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(); let dev = dev.into();
move |self_| Self { move |self_| Self {
dev: Some(dev.clone()), 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"; const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
/// Load MAC->name cache from disk /// 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 { match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other), Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()), 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) 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;
@@ -52,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();
+30 -14
View File
@@ -1,29 +1,44 @@
use axum::{ //! braindead version v0.1.x
Router, //!
routing::{get, post}, //! # 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; use tokio::net::TcpListener;
mod arpparse; mod arpparse;
pub mod assets;
mod dhcpparse; mod dhcpparse;
mod route; mod route;
mod utils; mod utils;
use std::io; use std::{env, io};
const MACHINE_NAME: &str = "lda.lan";
#[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, 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() let app = Router::new()
// .route("/home", get(home)) // .route("/home", get(home))
.route("/", get(home_2)) // .route("/", get(home_2))
.merge(home_2_route()) // .merge(home_2_route())
.route("/wake", post(wake_handler))
// .route("/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?;
@@ -32,6 +47,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());
-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::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::post; use axum::{extract::Path, response::Redirect};
use axum::{Router, extract::Path, response::Redirect, routing::get};
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 // Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly
pub async fn status_smart_redirect( 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> {
match serde_html_form::to_string(route::status_smart_redirect(q).await) { // no less bullshit
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))), let query = match parse_query(q).await {
Err(e) => Err(( QueryType::Ip(ip_addr) => DeviceQuery {
StatusCode::BAD_GATEWAY, filter: Filters {
Json(StatusError { ips: vec![ip_addr],
error: e.to_string(),
..Default::default() ..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 { pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
Redirect::permanent(&format!( Redirect::permanent(&format!(
"/api/status?name={name}", "/api/status?name={name}",
@@ -34,12 +63,9 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
)) ))
} }
pub fn api_router() -> Router { pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
Router::new() get_ips(&name).await.map_or_else(
.route("/status/{name}", get(status_redirect)) |e| ApiError::ise(e.to_string()).into_response(),
.route("/status", get(get_status_json)) |ips| Json(ips.collect::<Vec<_>>()).into_response(),
.route("/dhcp_leases", get(get_dhcp_leases)) )
.route("/smart/{q}", get(status_smart_redirect))
.route("/devs", get(devs_router))
.route("/wake", post(wake_multi))
} }
+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
+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}; use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
// DHCP lease endpoints // DHCP lease endpoints
@@ -14,21 +18,18 @@ pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl Into
.map(boolish_str) .map(boolish_str)
.unwrap_or(false); .unwrap_or(false);
match dhcpparse::read_dhcp_leases_with_names().await { match read_dhcp_leases_with_names().await {
Ok(leases_with_names) => { Ok(leases_with_names) => {
if !include_state { if !include_state {
return (StatusCode::OK, Json(leases_with_names)).into_response(); 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() (StatusCode::OK, Json(out)).into_response()
} }
Err(e) => ( Err(e) => ApiError {
StatusCode::BAD_GATEWAY, error: e.to_string(),
Json(StatusError { code: StatusCode::BAD_GATEWAY,
error: e.to_string(), }
..Default::default() .into_response(),
}),
)
.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::{ use crate::route::error::ApiError;
arpparse::NUDState,
utils::{
parse::{de_many, serialize_macs},
query::get_macs,
},
};
use axum::{Json, http::StatusCode, response::IntoResponse}; use axum::{Json, http::StatusCode, response::IntoResponse};
use axum_extra::extract::Query; 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 std::collections::HashSet; use serde_with::{DisplayFromStr, OneOrMany, serde_as};
use std::net::IpAddr; use std::net::IpAddr;
use crate::arpparse::NUDState;
use crate::utils::query::get_macs;
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
pub struct DeviceQuery { pub struct DeviceQuery {
pub name: Option<String>, pub name: Option<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde(flatten)]
pub ip: Vec<IpAddr>, pub filter: Filters,
#[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>,
} }
#[derive(Debug, Default, Clone, Hash, Deserialize)] #[derive(Debug, Default, Clone, Hash, Deserialize)]
@@ -43,100 +30,57 @@ pub struct Status<T> {
pub filters: Filters, pub filters: Filters,
} }
#[derive(Debug, Default, Serialize)] #[serde_as]
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct Filters { pub struct Filters {
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde_as(as = "OneOrMany<_>")]
pub ip: Vec<IpAddr>, #[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")] pub ips: Vec<IpAddr>,
pub dev: Vec<String>, #[serde_as(as = "OneOrMany<_>")]
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)]
pub nud: Vec<NUDState>, pub devs: Vec<String>,
#[serde( #[serde_as(as = "OneOrMany<_>")]
skip_serializing_if = "Vec::is_empty", #[serde(default)]
serialize_with = "serialize_macs" pub nuds: Vec<NUDState>,
)] #[serde_as(as = "OneOrMany<DisplayFromStr>")]
pub mac: Vec<MacAddr>, #[serde(default)]
} pub macs: Vec<MacAddr>,
#[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,
ip, filter: filters,
dev,
nud,
mac,
.. ..
}): Query<DeviceQuery>, }): Query<DeviceQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> { match get_macs(
if slice.is_empty() { name.as_slice(),
vec![None] &filters.ips,
} else { &filters.devs,
slice.iter().cloned().map(Some).collect() &filters.nuds,
} &filters.macs,
} )
let ips_opt = if ip.is_empty() { .await
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<_>>())
{ {
Ok(mut table) => { Ok(table) => (
if !mac.is_empty() { StatusCode::OK,
// mac here Json(Status {
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 {
name, name,
error: error.to_string(), table,
filters,
}), }),
) )
.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
+15 -21
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;
@@ -23,9 +23,8 @@ pub struct WakeResult {
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Clone, Copy)] #[derive(Debug, Serialize, Clone, Copy)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub ip: Option<IpAddr>, #[serde(flatten)]
#[serde(serialize_with = "ser_opm")] pub target: WakeTarget,
pub mac: Option<MacAddr>,
pub status: WakeTargetStatus, pub status: WakeTargetStatus,
} }
@@ -41,11 +40,11 @@ pub enum WakeTargetStatus {
} }
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Deserialize, Clone, Copy)] #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct WakeTarget { pub struct WakeTarget {
#[serde(default)] #[serde(default)]
pub ip: Option<IpAddr>, pub ip: Option<IpAddr>,
#[serde(default, deserialize_with = "des_opm")] #[serde(default, with = "mac::option_mac")]
pub mac: Option<MacAddr>, pub mac: Option<MacAddr>,
} }
@@ -82,21 +81,15 @@ pub async fn wake_multi_split(
.await?; .await?;
sock.set_broadcast(true)?; sock.set_broadcast(true)?;
Ok( let iter = targets.into_iter().map(async |c| {
futures::future::join_all(targets.into_iter().map(async |c| { if c.is_incomplete() {
if c.is_incomplete() { c.to_incomplete()
c.to_incomplete() } else {
} else { let t = c.try_into().expect("complete struct failed to try_into");
wake_one( wake_one(&sock, t).await.into()
&sock, }
c.try_into().expect("complete struct failed to try_into"), // type state pattern? });
) Ok(futures::future::join_all(iter).await)
.await
.into()
}
}))
.await,
)
} }
/* #[derive(Debug, Serialize)] /* #[derive(Debug, Serialize)]
pub struct WakeStatusLine { pub struct WakeStatusLine {
@@ -117,3 +110,4 @@ pub type WakeStatus = Status<WakeStatusLine>; */
// }): Query<DeviceQuery>, // }): Query<DeviceQuery>,
// ) -> impl IntoResponse { // ) -> 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::{ use crate::route::wake::{
WakeTarget as RouteWakeTarget, WakeTargetResult as RouteWakeResult, WakeTarget as RouteWakeTarget, WakeTargetResult as RouteWakeResult,
WakeTargetStatus as RouteWakeStatus, WakeTargetStatus as RouteWakeStatus,
@@ -22,25 +22,32 @@ impl TryFrom<RouteWakeTarget> for WakeTarget {
} }
} }
impl From<WakeTargetResult> for RouteWakeResult { impl From<WakeTarget> for RouteWakeTarget {
fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> Self { fn from(WakeTarget { ip, mac }: WakeTarget) -> Self {
Self { Self {
ip: Some(ip), ip: Some(ip),
mac: Some(mac), mac: Some(mac),
}
}
}
impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTargetResult { target, status }: WakeTargetResult) -> Self {
Self {
target: target.into(),
status: status.into(), status: status.into(),
} }
} }
} }
impl RouteWakeTarget { impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult { pub const fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult { RouteWakeResult {
ip: self.ip, target: self,
mac: self.mac,
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 {
-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)) && 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::*;
+2 -2
View File
@@ -7,7 +7,7 @@ use tokio::{
time::timeout, 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 { pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
timeout(Duration::from_secs(1), TcpStream::connect(addr)) timeout(Duration::from_secs(1), TcpStream::connect(addr))
@@ -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 { 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, Err(_) => 0,
Ok(l) => l Ok(l) => l
.into_iter() .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> { /// 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)
} }
+8 -10
View File
@@ -1,5 +1,6 @@
use crate::arpparse::NUDState; use crate::arpparse::NUDState;
use crate::dhcpparse::DhcpLeaseLine; use crate::dhcpparse::DhcpLeaseLine;
use crate::utils::query::get_macs;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::net::IpAddr; use std::net::IpAddr;
@@ -9,36 +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> {
use crate::utils::query::macs::get_macs;
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(None, Some(&ips), None, None).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()
+63 -152
View File
@@ -1,173 +1,84 @@
use crate::arpparse::{self, IpNeighLine, NUDState}; use lda_ipjs::subcommands::neighbor;
use crate::utils::{
cmd::exec_command,
error::{self, Error, Result},
};
use macaddr::MacAddr; use macaddr::MacAddr;
use crate::arpparse::{IpNeighLine, NUDState};
use anyhow::{Context, 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) -> 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)) 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()))
} }
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> { /// Query neighbor table with multi-filters. Empty slice = no filter.
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())
}
pub async fn get_macs( pub async fn get_macs(
machine_name: Option<&str>, machine_names: &[impl AsRef<str>],
ips: Option<&[IpAddr]>, ips: &[IpAddr],
dev: Option<&str>, devs: &[impl AsRef<str>],
state: Option<NUDState>, state: &[NUDState],
macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let ip_list: Option<Vec<IpAddr>> = // Resolve machine names to IPs
ips.map(|slice| slice.iter().map(|ip| ip.to_canonical()).collect()); let resolved_ips: HashSet<IpAddr> = if !machine_names.is_empty() {
let ip_list = match (ip_list, machine_name) { futures::future::try_join_all(machine_names.iter().map(|n| get_ips(n.as_ref())))
(Some(list), _) => list, .await?
(None, Some(name)) => get_ips(name).await?.collect(), .into_iter()
(None, None) => Vec::new(), .flatten()
}; .collect()
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())
} else { } 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( pub async fn get_mac(
ip: Option<IpAddr>, ip: Option<IpAddr>,
dev: Option<&str>, dev: Option<&str>,
state: Option<NUDState>, state: &[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]>,
) -> Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let mut ip_map: HashSet<IpAddr> = HashSet::new(); let ips: Vec<IpAddr> = ip.into_iter().collect();
let mut machine_map = HashSet::new(); let devs: Vec<&str> = dev.into_iter().collect();
if let Some(ips) = ips { let nud: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
ip_map.extend(ips);
}
if let Some(m) = machine_names {
machine_map.extend(get_ips(m).await.into_iter().flatten());
}
let real = match (ip_map.is_empty(), machine_map.is_empty()) { Ok(neighbor::nl::get(&ips, &devs, &nud, &[])
(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)
.await .await
.context("rtnetlink failed")?
.into_iter() .into_iter()
.flat_map(|f| f.into_iter().flatten()) .map(Into::into)
.collect() .collect())
} */ }
@@ -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::*;
+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 std::{io, net::IpAddr};
use futures::TryFutureExt; use futures::TryFutureExt;
use macaddr::MacAddr; use macaddr::MacAddr;
use tokio::net::UdpSocket; 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)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTarget { pub struct WakeTarget {
pub ip: IpAddr, pub ip: IpAddr,
@@ -41,8 +13,7 @@ pub struct WakeTarget {
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub ip: IpAddr, pub target: WakeTarget,
pub mac: MacAddr,
pub status: WakeStatus, pub status: WakeStatus,
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
@@ -52,22 +23,22 @@ 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.ip, self.mac, WakeStatus::Success) WakeTargetResult::new(self, WakeStatus::Success)
} }
fn bad(self) -> WakeTargetResult { const fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize) WakeTargetResult::new(self, WakeStatus::WrongSize)
} }
fn errored(self) -> WakeTargetResult { const fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress) WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
} }
} }
impl WakeTargetResult { impl WakeTargetResult {
fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self { const fn new(target: WakeTarget, status: WakeStatus) -> Self {
Self { ip, mac, status } 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); export const qs = new URLSearchParams(location.search);
@@ -21,7 +21,7 @@ export function setPill(kind, text) {
export function setLink(name, clear) { export function setLink(name, clear) {
const url = new URL(location.href); 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( /* const hasExtraFilters = filter_array.some(
(k) => url.searchParams.getAll(k).length (k) => url.searchParams.getAll(k).length
); );
+18 -1
View File
@@ -1,5 +1,18 @@
import { elLeases } from "./dom.js"; 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) { export function renderLeases(leases) {
if (!elLeases) return; if (!elLeases) return;
if (!Array.isArray(leases) || leases.length === 0) { if (!Array.isArray(leases) || leases.length === 0) {
@@ -24,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) {
+89 -3
View File
@@ -2,6 +2,7 @@ import { elHtml, elLog, setPill, qs, pill } from "./dom.js";
import { rankState } from "./utils.js"; import { rankState } from "./utils.js";
import { merge_wake_data, translate_wake_message } from "./wake.js"; import { merge_wake_data, translate_wake_message } from "./wake.js";
// IpNeighLine
const status_map = { const status_map = {
ip: "ip", ip: "ip",
mac: "mac", mac: "mac",
@@ -9,8 +10,14 @@ const status_map = {
dev: "interface", dev: "interface",
}; };
export const status_array = Object.keys(status_map); 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) { function buildStatusUrl(name) {
const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length); const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length);
if (name && !hasExtraFilters) { if (name && !hasExtraFilters) {
@@ -25,6 +32,25 @@ function buildStatusUrl(name) {
return u; 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) { export function renderStatus(data) {
const tbl = document.createElement("table"); const tbl = document.createElement("table");
tbl.className = "table"; tbl.className = "table";
@@ -82,12 +108,51 @@ export function renderStatus(data) {
if (r >= 5) setPill("ok", "online"); if (r >= 5) setPill("ok", "online");
else if (r >= 2) setPill("warn", "maybe"); else if (r >= 2) setPill("warn", "maybe");
else setPill("bad", "offline"); else setPill("bad", "offline");
if (data.filters.nud?.length > 0) pill.textContent += " (filtered)"; if (data.filters.nuds?.length > 0) pill.textContent += " (filtered)";
} else { } else {
setPill("warn", "unknown"); 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) { export async function fetchStatus(name, render = true, data_wake) {
setPill("warn", "checking…"); setPill("warn", "checking…");
const u = buildStatusUrl(name); const u = buildStatusUrl(name);
@@ -97,6 +162,7 @@ export async function fetchStatus(name, render = true, data_wake) {
if (!r.ok) { if (!r.ok) {
let msg = String(r.status); let msg = String(r.status);
try { try {
/** @type {{error: string}} */
const err = await r.clone().json(); const err = await r.clone().json();
msg = err.error || JSON.stringify(err); msg = err.error || JSON.stringify(err);
} catch { } catch {
@@ -106,6 +172,26 @@ export async function fetchStatus(name, render = true, data_wake) {
setPill("bad", "error"); setPill("bad", "error");
return; 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(); const data = await r.json();
if (data_wake) { if (data_wake) {
+7
View File
@@ -15,6 +15,13 @@ body {
padding: 0; padding: 0;
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
} }
@media screen and (max-width: 768px) {
wrap {
margin-inline: 8px;
}
}
body { body {
display: flex; display: flex;
min-height: 100dvh; min-height: 100dvh;
+31 -3
View File
@@ -1,6 +1,9 @@
import { elHtml, elLog, setPill } from "./dom.js"; import { elHtml, elLog, setPill } from "./dom.js";
import { fetchStatus, renderStatus } from "./status.js"; import { fetchStatus, renderStatus } from "./status.js";
/**
* @param {String} name just plain name
*/
export async function sendWake(name) { export async function sendWake(name) {
const data = await fetchStatus(name, false); const data = await fetchStatus(name, false);
if (!data) return; // can not proceed; theres nothing. if (!data) return; // can not proceed; theres nothing.
@@ -51,8 +54,29 @@ export async function sendWake(name) {
/** /**
* *
* @param {Array} table * @param {{
* @param {Array} wake * 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) { export function merge_wake_data(table, wake) {
if (!wake) return table; if (!wake) return table;
@@ -90,7 +114,11 @@ export function merge_wake_data(table, wake) {
return return_array; return return_array;
} }
/**
*
* @param {"incomplete" | "succeed" | "nonexistent_address" | "wrong_size" | any} wake_msg
* @returns {string}
*/
export function translate_wake_message(wake_msg) { export function translate_wake_message(wake_msg) {
switch (wake_msg) { switch (wake_msg) {
case "incomplete": case "incomplete":
+2 -3
View File
@@ -27,11 +27,10 @@
<div class="row muted" style="gap: 16px"> <div class="row muted" style="gap: 16px">
<span <span
>Uses query >Uses query
<span title="available keys: name, ip, mac, dev, nud" <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>