Compare commits
28
Commits
v0.1.4
..
00cf33eefb
No files matched your search
+7
-2
@@ -1,5 +1,10 @@
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
linker = "rust-lld"
|
||||
|
||||
[build]
|
||||
target = "armv7-unknown-linux-musleabihf"
|
||||
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
linker = "rust-lld"
|
||||
[alias]
|
||||
ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
|
||||
t = "test -- --nocapture --test-threads=1"
|
||||
tdebug = "test -- --nocapture --test-threads=1 --show-output"
|
||||
File renamed without changes.
@@ -1,8 +1,10 @@
|
||||
you should use block code to refence code, like so:
|
||||
|
||||
```rust
|
||||
hello_world!(println);
|
||||
// indented lines
|
||||
```
|
||||
|
||||
in the chat. Else it fucks up formatting.
|
||||
|
||||
We work towards clean, maintainable design. im not the best at this thing.
|
||||
Vendored
+2
-1
@@ -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
+136
-3
@@ -32,6 +32,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@@ -189,6 +195,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
@@ -610,6 +622,22 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lda-ipjs"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
"macaddr",
|
||||
"rtnetlink",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"strum",
|
||||
"thiserror 2.0.16",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.175"
|
||||
@@ -669,6 +697,66 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-packet-core"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4"
|
||||
dependencies = [
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-packet-route"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ec2f5b6839be2a19d7fa5aab5bc444380f6311c2b693551cb80f45caaa7b5ef"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-proto"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-sys",
|
||||
"thiserror 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netlink-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
"libc",
|
||||
"log",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
@@ -705,6 +793,12 @@ version = "4.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
@@ -767,6 +861,23 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtnetlink"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08fd15aa4c64c34d0b3178e45ec6dad313a9f02b193376d501668a7950264bb7"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"netlink-packet-core",
|
||||
"netlink-packet-route",
|
||||
"netlink-proto",
|
||||
"netlink-sys",
|
||||
"nix",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
@@ -999,13 +1110,33 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1179,19 +1310,21 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "wakey"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"color-eyre",
|
||||
"futures",
|
||||
"lda-ipjs",
|
||||
"macaddr",
|
||||
"serde",
|
||||
"serde_html_form",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"strum",
|
||||
"thiserror",
|
||||
"thiserror 2.0.16",
|
||||
"tokio",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
+17
-3
@@ -1,10 +1,11 @@
|
||||
[package]
|
||||
name = "wakey"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
edition = "2024"
|
||||
publish = ["gitea"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
axum = { version = "0.8.4", features = ["macros"] }
|
||||
axum-extra = { version = "0.10.1", features = ["query"] }
|
||||
color-eyre = "0.6.5"
|
||||
@@ -16,7 +17,13 @@ serde_json = "1.0.143"
|
||||
serde_with = { version = "3.14.0", features = ["json"] }
|
||||
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
|
||||
thiserror = "2.0.16"
|
||||
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] }
|
||||
tokio = { version = "1.47.1", features = [
|
||||
"fs",
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
"io-util",
|
||||
"macros",
|
||||
] }
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
[profile.release]
|
||||
@@ -25,4 +32,11 @@ strip = true
|
||||
|
||||
[features]
|
||||
default = ["very-smart-parsing"]
|
||||
very-smart-parsing = [] # this is the a-bit-redundant parse thing that copilot made
|
||||
very-smart-parsing = [
|
||||
] # this is the a-bit-redundant parse thing that copilot made
|
||||
|
||||
[workspace]
|
||||
members = ["ipjs"]
|
||||
|
||||
[dependencies.lda-ipjs]
|
||||
path = "ipjs"
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "lda-ipjs"
|
||||
description = "ip -j show schemas"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] }
|
||||
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
|
||||
serde_json = "1.0.143"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_with = { version = "3.14.0", features = ["json"] }
|
||||
thiserror = "2.0.16"
|
||||
anyhow = "1.0.100"
|
||||
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util", "macros"] }
|
||||
rtnetlink = "0.18.1"
|
||||
futures = "0.3.31"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
trait IpCommand {
|
||||
|
||||
}
|
||||
Whitespace-only changes.
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Ok(serde_json::from_slice(&output.stdout)?)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//! ts
|
||||
//!
|
||||
//! deals with both ip a (addroutput) and ip l (commonoutput)
|
||||
//!
|
||||
//! lowk why its free but its indirection and its ass
|
||||
|
||||
pub mod json;
|
||||
|
||||
use crate::utils::serialize::mac::option_mac;
|
||||
use macaddr::MacAddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// i dont include what i dont know about (almost all ts)
|
||||
#[derive(Serialize, Debug, Deserialize)]
|
||||
pub struct AddrOutput {
|
||||
pub ifindex: u32,
|
||||
pub ifname: String,
|
||||
/// i imagine UP or DOWN, unknown
|
||||
pub operstate: String,
|
||||
// 6 has a serde and the enum doesnt? why. (serializing ts is ass although... im not given an array. they string formatted ts)
|
||||
#[serde(with = "option_mac", default)]
|
||||
pub address: Option<MacAddr>,
|
||||
#[serde(default)] // i wish we have intellisense for this... fuck you metaprogramming
|
||||
pub addr_info: Vec<AddrInfo>,
|
||||
}
|
||||
|
||||
// i be copying
|
||||
// Raw JSON shape from ip -j -4 address show
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct AddrInfo {
|
||||
pub family: Option<String>,
|
||||
|
||||
pub local: Option<String>,
|
||||
pub prefixlen: Option<u8>,
|
||||
|
||||
pub broadcast: Option<String>,
|
||||
|
||||
pub scope: Option<String>,
|
||||
pub label: Option<String>,
|
||||
// many more exist; we only take what we need
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod address;
|
||||
pub mod neighbor;
|
||||
@@ -0,0 +1,48 @@
|
||||
//! 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 {
|
||||
Ok(serde_json::from_slice(&output.stdout)?)
|
||||
}
|
||||
}
|
||||
|
||||
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(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
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! ```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,
|
||||
pub dev: String,
|
||||
#[serde(with = "option_mac", default, rename(deserialize = "lladdr"))]
|
||||
pub mac: Option<MacAddr>,
|
||||
#[serde(default)]
|
||||
pub state: Vec<NUDState>,
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//! this is purely experimental. im not doing ts no mo
|
||||
|
||||
// hallo
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
net::IpAddr,
|
||||
};
|
||||
|
||||
use futures::TryStreamExt;
|
||||
use macaddr::MacAddr;
|
||||
use rtnetlink::packet_route::{
|
||||
AddressFamily,
|
||||
link::LinkAttribute,
|
||||
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
|
||||
};
|
||||
|
||||
use super::{NUDState, NeighborItem};
|
||||
|
||||
// dont you love https://github.com/rust-netlink/rtnetlink/blob/main/examples/get_neighbours.rs
|
||||
pub async fn get(
|
||||
ip: Option<IpAddr>,
|
||||
dev: Option<&str>,
|
||||
nud: &[NUDState],
|
||||
) -> anyhow::Result<Vec<NeighborItem>> {
|
||||
let (gip, gdev, gnud) = (ip, dev, nud);
|
||||
let (conn, handle, _) = rtnetlink::new_connection()?;
|
||||
tokio::spawn(conn); // every time?
|
||||
let mut neighbor_data = handle.neighbours().get().execute();
|
||||
let nudset: HashSet<&NUDState> = HashSet::from_iter(gnud);
|
||||
let mut ball: HashMap<u32, String> = HashMap::new();
|
||||
let mut result = vec![];
|
||||
'big: while let Some(neighbour_message_item) = neighbor_data.try_next().await? {
|
||||
// Filter by address family
|
||||
if !matches!(
|
||||
neighbour_message_item.header.family,
|
||||
AddressFamily::Inet | AddressFamily::Inet6
|
||||
) || matches!(neighbour_message_item.header.state, NeighbourState::Noarp)
|
||||
// copilot says this to match ip -j n s
|
||||
{
|
||||
continue 'big;
|
||||
}
|
||||
|
||||
let state = vec![
|
||||
neighbour_message_item
|
||||
.header
|
||||
.state
|
||||
.try_into()
|
||||
.unwrap_or_default(),
|
||||
]; // ONE ITEM. why tf ts design json.
|
||||
let mut ip = None;
|
||||
let mut mac = None;
|
||||
|
||||
for neigh_attr in neighbour_message_item.attributes {
|
||||
match neigh_attr {
|
||||
NeighbourAttribute::Destination(neighbour_address) => match neighbour_address {
|
||||
NeighbourAddress::Inet(ipv4_addr) => ip = Some(ipv4_addr.into()),
|
||||
NeighbourAddress::Inet6(ipv6_addr) => ip = Some(ipv6_addr.into()),
|
||||
_ => continue 'big,
|
||||
},
|
||||
NeighbourAttribute::LinkLocalAddress(items) => {
|
||||
mac = match items.len() {
|
||||
6 => items.first_chunk::<6>().map(|&e| MacAddr::from(e)),
|
||||
8 => items.first_chunk::<8>().map(|&e| MacAddr::from(e)),
|
||||
_ => continue 'big,
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
// exquisite
|
||||
let dev = if let Some(cached) = ball.get(&neighbour_message_item.header.ifindex) {
|
||||
Some(cached.clone())
|
||||
} else {
|
||||
// Query and cache
|
||||
let name = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_index(neighbour_message_item.header.ifindex)
|
||||
.execute()
|
||||
.try_next()
|
||||
.await?
|
||||
.and_then(|a| {
|
||||
a.attributes.into_iter().find_map(|attr| match attr {
|
||||
LinkAttribute::IfName(name) => Some(name),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(ref n) = name {
|
||||
ball.insert(neighbour_message_item.header.ifindex, n.clone());
|
||||
}
|
||||
name
|
||||
};
|
||||
|
||||
let Some((ip, dev)) = ip.zip(dev) else {
|
||||
continue 'big;
|
||||
};
|
||||
|
||||
{
|
||||
// low block
|
||||
if let Some(fip) = gip
|
||||
&& fip != ip
|
||||
{
|
||||
continue 'big;
|
||||
}
|
||||
if let Some(fdev) = gdev
|
||||
&& dev != fdev
|
||||
{
|
||||
continue 'big;
|
||||
}
|
||||
if !nudset.is_empty() && !nudset.contains(&state[0]) {
|
||||
continue 'big;
|
||||
};
|
||||
}
|
||||
|
||||
result.push(NeighborItem {
|
||||
ip,
|
||||
dev,
|
||||
mac,
|
||||
state,
|
||||
});
|
||||
}
|
||||
Ok(result) // now i need another pass to filter out the uh.
|
||||
}
|
||||
|
||||
impl TryFrom<NeighbourState> for NUDState {
|
||||
fn try_from(value: NeighbourState) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
NeighbourState::Incomplete => Ok(Self::Incomplete),
|
||||
NeighbourState::Reachable => Ok(Self::Reachable),
|
||||
NeighbourState::Stale => Ok(Self::Stale),
|
||||
NeighbourState::Delay => Ok(Self::Delay),
|
||||
NeighbourState::Probe => Ok(Self::Probe),
|
||||
NeighbourState::Failed => Ok(Self::Failed),
|
||||
NeighbourState::Noarp => Ok(Self::Noarp),
|
||||
NeighbourState::Permanent => Ok(Self::Permanent),
|
||||
NeighbourState::None => Ok(Self::None),
|
||||
NeighbourState::Other(e) => Ok(Self::Other(e)),
|
||||
_ => Err(u16::MAX), // idk
|
||||
}
|
||||
}
|
||||
|
||||
type Error = u16;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod iter;
|
||||
pub mod serialize;
|
||||
@@ -0,0 +1,46 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod mac;
|
||||
// pub mod vec;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use lda_ipjs::subcommands::neighbor;
|
||||
|
||||
#[tokio::test] // ← Use tokio::test instead of manual #[tokio::main]
|
||||
async fn ball1() -> anyhow::Result<()> {
|
||||
let result = neighbor::nl::get(None, None, &[]).await?;
|
||||
println!("netlink results: {:?}", result);
|
||||
Ok(()) // ← Don't force error, let it succeed
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ball2() -> anyhow::Result<()> {
|
||||
let result = neighbor::json::get(None, None, &[]).await?;
|
||||
println!("json results: {:?}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Add this to debug the raw JSON
|
||||
#[tokio::test]
|
||||
async fn ball_raw_json() -> anyhow::Result<()> {
|
||||
let output = tokio::process::Command::new("ip")
|
||||
.args(["-j", "neigh", "show"])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let json = String::from_utf8_lossy(&output.stdout);
|
||||
println!("Raw JSON:\n{}", json);
|
||||
|
||||
// Try to parse it
|
||||
let parsed: Result<Vec<neighbor::NeighborItem>, _> = serde_json::from_slice(&output.stdout);
|
||||
match parsed {
|
||||
Ok(items) => println!("Parsed {} items", items.len()),
|
||||
Err(e) => println!("Parse error: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Check what fields actually exist in the JSON
|
||||
#[tokio::test]
|
||||
async fn ball_field_analysis() -> anyhow::Result<()> {
|
||||
let output = tokio::process::Command::new("ip")
|
||||
.args(["-j", "neigh", "show"])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let raw: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout)?;
|
||||
|
||||
println!("Found {} neighbor entries", raw.len());
|
||||
|
||||
// Collect all unique field names across all entries
|
||||
let mut all_fields = std::collections::HashSet::new();
|
||||
for (i, entry) in raw.iter().enumerate() {
|
||||
if let Some(obj) = entry.as_object() {
|
||||
println!("\nEntry {}: {} fields", i, obj.len());
|
||||
for (key, value) in obj {
|
||||
all_fields.insert(key.clone());
|
||||
println!(" {}: {} = {:?}", key, value.type_name(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n=== All unique fields seen ===");
|
||||
for field in &all_fields {
|
||||
println!(" - {}", field);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper trait to get type name for JSON values
|
||||
trait TypeName {
|
||||
fn type_name(&self) -> &str;
|
||||
}
|
||||
|
||||
impl TypeName for serde_json::Value {
|
||||
fn type_name(&self) -> &str {
|
||||
match self {
|
||||
serde_json::Value::Null => "null",
|
||||
serde_json::Value::Bool(_) => "bool",
|
||||
serde_json::Value::Number(_) => "number",
|
||||
serde_json::Value::String(_) => "string",
|
||||
serde_json::Value::Array(_) => "array",
|
||||
serde_json::Value::Object(_) => "object",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test filtering logic
|
||||
#[tokio::test]
|
||||
async fn ball_compare_backends() -> anyhow::Result<()> {
|
||||
println!("=== JSON Backend ===");
|
||||
let json_result = neighbor::json::get(None, None, &[]).await?;
|
||||
println!("Got {} entries from JSON", json_result.len());
|
||||
|
||||
println!("\n=== Netlink Backend ===");
|
||||
let nl_result = neighbor::nl::get(None, None, &[]).await?;
|
||||
println!("Got {} entries from netlink", nl_result.len());
|
||||
|
||||
// Compare counts
|
||||
if json_result.len() != nl_result.len() {
|
||||
println!(
|
||||
"\n⚠️ Count mismatch! JSON: {}, Netlink: {}",
|
||||
json_result.len(),
|
||||
nl_result.len()
|
||||
);
|
||||
} else {
|
||||
println!("\n✅ Both backends returned same count");
|
||||
}
|
||||
let a: HashSet<neighbor::NeighborItem> = HashSet::from_iter(json_result);
|
||||
let b: HashSet<neighbor::NeighborItem> = HashSet::from_iter(nl_result);
|
||||
println!(
|
||||
"istg {len1} == {len2} or else",
|
||||
len1 = a.len(),
|
||||
len2 = b.len()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cidr_filter() {
|
||||
unimplemented!("never. i aint add what i dont need")
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# taking back my code AHHHH
|
||||
|
||||
this is going to be lengthy. ci is spotless but idk how it works cuz powershell. this is good but i cant afford VPN on the router; we will need to do some bullshit.
|
||||
|
||||
## overview
|
||||
|
||||
we need a:
|
||||
|
||||
- small ahh bin in the router.
|
||||
- forwards a shell and some functions.
|
||||
- may be shell calling fns maybe some unsafe c. i cant unsafe c.
|
||||
- big ahh bin in the vps.
|
||||
- connect to the small ahh bin thru the wide ass internet.
|
||||
- forwards some APIs.
|
||||
- static site
|
||||
- now need no hosts; apis can be called anywhere anyway.
|
||||
- do offer these in the big ahh bins tho lets not miss out some shit.
|
||||
- automation
|
||||
- gitea
|
||||
- on the vps or the uh idk.
|
||||
- ABSOLUTELY can auto build main:latest every day / other day.
|
||||
- and releases.
|
||||
- router
|
||||
- a script to pull the tar. from actions artifacts or from releases. (lua or shell yo)
|
||||
- a script for the bin and the init.d scripts for these two.
|
||||
- vps
|
||||
- a systemd service (ima use ai for ts ngl)
|
||||
- an auto updater. maybe we overkill and dnf. maybe go easy and git clone + cargo install. maybe we pull from release or nightly. idk
|
||||
|
||||
i think idk
|
||||
|
||||
## router
|
||||
|
||||
### current
|
||||
|
||||
everything.
|
||||
|
||||
we have some functions that call ip neigh a bajillion times.
|
||||
we have some other functions that read the same file/folder in /sys or sum shit.
|
||||
we have a function that broadcast to a set ahh address.
|
||||
|
||||
we have some routes; and some data structures acting as schema for them ahh. we dont do posts with json but gets with query strings for some ahh reasons.
|
||||
|
||||
these are carryover from the early days.
|
||||
|
||||
also from the early days are the js.
|
||||
|
||||
static was 100% ai with 0 intervention.
|
||||
|
||||
we have a search bar of sort that hooks through a smart endpoint that searches for everything.
|
||||
|
||||
we have some bullshit. and two buttons to check and wake. that pings `/api/smart/{query}` and `/wake?name=query`. why.
|
||||
we have the result of the wake and or status and the errors in the first text box.
|
||||
|
||||
the list of cliets with names connected to the router the second box.
|
||||
thats cool.
|
||||
|
||||
### changes?
|
||||
|
||||
idk changes.
|
||||
@@ -10,12 +10,12 @@
|
||||
START=50 # Run before Tailscale (typically START~80)
|
||||
|
||||
start() {
|
||||
LOGFILE=/var/log/update_tailscale.log
|
||||
MAX_RETRIES=10
|
||||
RETRY_COUNT=0
|
||||
LOGFILE=/var/log/update_tailscale.log
|
||||
MAX_RETRIES=10
|
||||
RETRY_COUNT=0
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
nohup sh -c '
|
||||
# shellcheck disable=SC2016
|
||||
sh -c '
|
||||
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
|
||||
if fn; then
|
||||
echo "[update_tailscale] Success at $(date)" >>"$LOGFILE"
|
||||
|
||||
@@ -8,14 +8,14 @@ NAME=wakey
|
||||
BIN=/root/.bin/wakey
|
||||
|
||||
start_service() {
|
||||
procd_open_instance
|
||||
procd_set_param command "$BIN"
|
||||
procd_set_param respawn 5 1 0
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
procd_open_instance
|
||||
procd_set_param command "$BIN"
|
||||
procd_set_param respawn 5 1 0
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
: # procd manages the process; nothing to do here
|
||||
: # procd manages the process; nothing to do here
|
||||
}
|
||||
@@ -6,7 +6,7 @@ pids="$(pidof wakey 2>/dev/null)"
|
||||
|
||||
# 2) Fallback: ps | awk (avoid matching awk/grep themselves)
|
||||
if [ -z "$pids" ]; then
|
||||
pids="$(ps w 2>/dev/null | awk '/\/\.bin\/wakey/ && $0 !~ /awk/ {print $1}')"
|
||||
pids="$(ps w 2>/dev/null | awk '/\/\.bin\/wakey/ && $0 !~ /awk/ {print $1}')"
|
||||
fi
|
||||
|
||||
[ -n "$pids" ] || exit 0
|
||||
@@ -18,7 +18,7 @@ kill -TERM "$pids" 2>/dev/null || true
|
||||
usleep 250000 # uhhh sleep is stupid
|
||||
remain=""
|
||||
for p in $pids; do
|
||||
kill -0 "$p" 2>/dev/null && remain="$remain $p"
|
||||
kill -0 "$p" 2>/dev/null && remain="$remain $p"
|
||||
done
|
||||
[ -z "$remain" ] || kill -KILL "$remain" 2>/dev/null || true
|
||||
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
set -e
|
||||
# 0th step from lda is to move the pre assed resolv.conf to the real resolv.conf
|
||||
if [ -f /tmp/resolv.conf ]; then
|
||||
cp -af /tmp/resolv.conf /etc/resolv.conf
|
||||
cp -af /tmp/resolv.conf /etc/resolv.conf
|
||||
elif [ -f /rom/etc/resolv.conf ]; then
|
||||
cp -af /rom/etc/resolv.conf /etc/resolv.conf
|
||||
cp -af /rom/etc/resolv.conf /etc/resolv.conf
|
||||
fi
|
||||
|
||||
if [ -x "$(which tailscale)" ]; then
|
||||
tailscale update
|
||||
exit 0
|
||||
tailscale update
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. Fetch the latest version number from Tailscale's site
|
||||
LATEST=$(curl -s https://pkgs.tailscale.com/stable/ | grep -Eo 'tailscale_[0-9\.]+_arm.tgz' | head -n1)
|
||||
|
||||
[ -z "$LATEST" ] && {
|
||||
echo "❌ Couldn't find latest ARM binary."
|
||||
exit 1
|
||||
echo "❌ Couldn't find latest ARM binary."
|
||||
exit 1
|
||||
}
|
||||
|
||||
URL="https://pkgs.tailscale.com/stable/${LATEST}"
|
||||
@@ -26,8 +26,8 @@ URL="https://pkgs.tailscale.com/stable/${LATEST}"
|
||||
cd /var/tmp
|
||||
|
||||
if [ -f "$LATEST" ]; then
|
||||
echo "file exists"
|
||||
rm "$LATEST"
|
||||
echo "file exists"
|
||||
rm "$LATEST"
|
||||
fi
|
||||
|
||||
echo "Downloading $LATEST..."
|
||||
|
||||
@@ -24,113 +24,113 @@ STAGING="$TMPDIR/wakey-rootfs.$$.$ARCH"
|
||||
|
||||
log() { echo "[update_wakey] $*"; }
|
||||
fail() {
|
||||
echo "[update_wakey] ERROR: $*" >&2
|
||||
exit 1
|
||||
echo "[update_wakey] ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
http_get() {
|
||||
# http_get <url>
|
||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||
# uclient-fetch supports -O -, write to stdout
|
||||
uclient-fetch -O - "$1" 2>/dev/null || return 1
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
# shellcheck disable=SC2086
|
||||
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${WAKEY_INSECURE:-}" ]; then
|
||||
curl -fsSL -k "$1" || return 1
|
||||
else
|
||||
curl -fsSL "$1" || return 1
|
||||
fi
|
||||
else
|
||||
return 2
|
||||
fi
|
||||
# http_get <url>
|
||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||
# uclient-fetch supports -O -, write to stdout
|
||||
uclient-fetch -O - "$1" 2>/dev/null || return 1
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
# shellcheck disable=SC2086
|
||||
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${WAKEY_INSECURE:-}" ]; then
|
||||
curl -fsSL -k "$1" || return 1
|
||||
else
|
||||
curl -fsSL "$1" || return 1
|
||||
fi
|
||||
else
|
||||
return 2
|
||||
fi
|
||||
}
|
||||
|
||||
fetch() {
|
||||
# fetch <url> <out>
|
||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||
uclient-fetch -O "$2" "$1" || return 1
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${WAKEY_INSECURE:-}" ]; then
|
||||
curl -fSL -k -o "$2" "$1" || return 1
|
||||
else
|
||||
curl -fSL -o "$2" "$1" || return 1
|
||||
fi
|
||||
else
|
||||
return 2
|
||||
fi
|
||||
# fetch <url> <out>
|
||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||
uclient-fetch -O "$2" "$1" || return 1
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${WAKEY_INSECURE:-}" ]; then
|
||||
curl -fSL -k -o "$2" "$1" || return 1
|
||||
else
|
||||
curl -fSL -o "$2" "$1" || return 1
|
||||
fi
|
||||
else
|
||||
return 2
|
||||
fi
|
||||
}
|
||||
|
||||
latest_asset_url() {
|
||||
# prints URL to stdout, returns 0 on success (public release only)
|
||||
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
|
||||
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
|
||||
RESP=$(http_get "$API") || return 1
|
||||
# Extract the first browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
|
||||
echo "$RESP" |
|
||||
grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' |
|
||||
head -n1 |
|
||||
cut -d '"' -f 4
|
||||
# prints URL to stdout, returns 0 on success (public release only)
|
||||
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
|
||||
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
|
||||
RESP=$(http_get "$API") || return 1
|
||||
# Extract the first browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
|
||||
echo "$RESP" |
|
||||
grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' |
|
||||
head -n1 |
|
||||
cut -d '"' -f 4
|
||||
}
|
||||
|
||||
main() {
|
||||
[ -f "$FLAG" ] && return
|
||||
URL="${WAKEY_TGZ_URL:-}"
|
||||
if [ -z "$URL" ]; then
|
||||
HOST=${WAKEY_HOST:-git.ldlda.com}
|
||||
OWNER=${WAKEY_OWNER:-lda}
|
||||
REPO=${WAKEY_REPO:-wakey}
|
||||
if [ -n "${WAKEY_VERSION:-}" ]; then
|
||||
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
|
||||
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
|
||||
else
|
||||
URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL"
|
||||
[ -n "$URL" ] || fail "no asset URL found for arch $ARCH"
|
||||
fi
|
||||
fi
|
||||
[ -f "$FLAG" ] && return
|
||||
URL="${WAKEY_TGZ_URL:-}"
|
||||
if [ -z "$URL" ]; then
|
||||
HOST=${WAKEY_HOST:-git.ldlda.com}
|
||||
OWNER=${WAKEY_OWNER:-lda}
|
||||
REPO=${WAKEY_REPO:-wakey}
|
||||
if [ -n "${WAKEY_VERSION:-}" ]; then
|
||||
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
|
||||
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
|
||||
else
|
||||
URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL"
|
||||
[ -n "$URL" ] || fail "no asset URL found for arch $ARCH"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "fetching $URL"
|
||||
if ! fetch "$URL" "$TMPFILE"; then
|
||||
fail "download failed"
|
||||
fi
|
||||
log "fetching $URL"
|
||||
if ! fetch "$URL" "$TMPFILE"; then
|
||||
fail "download failed"
|
||||
fi
|
||||
|
||||
log "installing"
|
||||
mkdir -p "$TMPDIR" "$STAGING"
|
||||
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
|
||||
log "installing"
|
||||
mkdir -p "$TMPDIR" "$STAGING"
|
||||
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
|
||||
|
||||
# Ensure execute bits on staged files we know should be executable
|
||||
for f in \
|
||||
"$STAGING/etc/init.d/"* \
|
||||
"$STAGING/etc/ldlda_help/"*.sh \
|
||||
"$STAGING/root/.bin/wakey" \
|
||||
"$STAGING/root/.bin/kill_wakey.sh" \
|
||||
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do
|
||||
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
|
||||
done
|
||||
# Ensure execute bits on staged files we know should be executable
|
||||
for f in \
|
||||
"$STAGING/etc/init.d/"* \
|
||||
"$STAGING/etc/ldlda_help/"*.sh \
|
||||
"$STAGING/root/.bin/wakey" \
|
||||
"$STAGING/root/.bin/kill_wakey.sh" \
|
||||
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do
|
||||
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
|
||||
for f in \
|
||||
"$STAGING/etc/init.d/"* \
|
||||
"$STAGING/etc/ldlda_help/"*.sh \
|
||||
"$STAGING/root/.bin/"*.sh; do
|
||||
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
|
||||
done
|
||||
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
|
||||
for f in \
|
||||
"$STAGING/etc/init.d/"* \
|
||||
"$STAGING/etc/ldlda_help/"*.sh \
|
||||
"$STAGING/root/.bin/"*.sh; do
|
||||
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Copy staged tree into /
|
||||
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
|
||||
# Copy staged tree into /
|
||||
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
|
||||
|
||||
rm -f "$TMPFILE"
|
||||
rm -rf "$STAGING"
|
||||
rm -f "$TMPFILE"
|
||||
rm -rf "$STAGING"
|
||||
|
||||
if [ -f /etc/init.d/wakey ]; then
|
||||
/etc/init.d/wakey enable || true
|
||||
/etc/init.d/wakey restart || /etc/init.d/wakey start || true
|
||||
fi
|
||||
touch "$FLAG"
|
||||
log "done"
|
||||
if [ -f /etc/init.d/wakey ]; then
|
||||
/etc/init.d/wakey enable || true
|
||||
/etc/init.d/wakey restart || /etc/init.d/wakey start || true
|
||||
fi
|
||||
touch "$FLAG"
|
||||
log "done"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
+186
-39
@@ -1,22 +1,25 @@
|
||||
"thanks chatgpt"
|
||||
|
||||
import os
|
||||
import textwrap
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from typing import Callable
|
||||
|
||||
root = Path(__file__).parent.parent
|
||||
static = root / "static" # change "assets" to your folder
|
||||
out_rs = root / "src" / "assets.rs"
|
||||
static = root / "static"
|
||||
src = root / "src"
|
||||
out_rs = src / "assets.rs"
|
||||
|
||||
assert 'name = "wakey"' in (root / "Cargo.toml").read_text(encoding="utf-8"), (
|
||||
"uhh how do i explain this"
|
||||
)
|
||||
|
||||
|
||||
def sanitize(name: str) -> str:
|
||||
# Valid Rust identifiers: letters, digits, underscores; no starting digit
|
||||
out = []
|
||||
for c in name:
|
||||
if c.isalnum() or c == "_":
|
||||
out.append(c)
|
||||
else:
|
||||
out.append("_")
|
||||
s = "".join(out)
|
||||
s = "".join(c if c.isalnum() else "_" for c in name)
|
||||
if s and s[0].isdigit():
|
||||
s = "_" + s
|
||||
return s
|
||||
@@ -24,42 +27,186 @@ def sanitize(name: str) -> str:
|
||||
|
||||
def indent(text: str, n: int) -> str:
|
||||
pad = " " * n
|
||||
return "\n".join(pad + line if line.strip() else line for line in text.splitlines())
|
||||
return textwrap.indent(text, pad)
|
||||
|
||||
|
||||
class RsAssetFile:
|
||||
def __init__(self, path: Path):
|
||||
class RsAsset(ABC):
|
||||
path: Path
|
||||
|
||||
|
||||
class RsAssetWithName(ABC):
|
||||
name: str
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def name_gen(path: Path) -> str:
|
||||
"helper"
|
||||
|
||||
|
||||
class RsAssetFile(RsAsset, RsAssetWithName):
|
||||
__match_args__ = ("path", "name")
|
||||
|
||||
def __init__(self, path: Path, name: str | None = None):
|
||||
self.path = path
|
||||
if name is None:
|
||||
self.name: str = RsAssetFile.name_gen(path)
|
||||
else:
|
||||
self.name = name
|
||||
|
||||
def path_relative_to(self, path: Path):
|
||||
"helper"
|
||||
return self.path.relative_to(path, walk_up=True).as_posix()
|
||||
|
||||
@staticmethod
|
||||
def plain_template(const_name: str, relative_path: str):
|
||||
return f'pub const {const_name}: &str = include_str!("{relative_path}");'
|
||||
|
||||
@staticmethod
|
||||
def macroed_template(const_name: str, relative_path: str):
|
||||
return f'file {const_name} "{relative_path}"'
|
||||
|
||||
@staticmethod
|
||||
def name_gen(path: Path) -> str:
|
||||
return sanitize(path.name).upper()
|
||||
|
||||
|
||||
class RsAssetModule(RsAsset, RsAssetWithName):
|
||||
__match_args__ = ("path", "name")
|
||||
|
||||
def __init__(self, path: Path, name: str | None = None):
|
||||
self.path = path
|
||||
if name is None:
|
||||
self.name: str = RsAssetModule.name_gen(path)
|
||||
else:
|
||||
self.name = name
|
||||
|
||||
@staticmethod
|
||||
def plain_template(sanitized_name: str, body: str):
|
||||
"braindead"
|
||||
indented_body = indent(body, 4)
|
||||
return f"pub mod {sanitized_name} {{\n{indented_body}\n}}"
|
||||
|
||||
@staticmethod
|
||||
def macroed_template(sanitized_name: str, body: str):
|
||||
indented_body = indent(body, 4)
|
||||
return f"folder {sanitized_name} {{\n{indented_body}\n}}"
|
||||
|
||||
@staticmethod
|
||||
def process_body(path: Path, renderer: Callable[[RsAsset], str]):
|
||||
"here just cuz. Path has to be a folder... so idk"
|
||||
body = RsAssetModule.iterate_assets(path)
|
||||
return "\n".join(map(renderer, body))
|
||||
|
||||
@staticmethod
|
||||
def iterate_assets(path: Path) -> "Iterable[RsAssetFile | RsAssetModule]":
|
||||
subs = []
|
||||
for f in path.iterdir():
|
||||
if f.is_file():
|
||||
yield RsAssetFile(f)
|
||||
elif f.is_dir():
|
||||
subs.append(RsAssetModule(f))
|
||||
yield from subs
|
||||
|
||||
@staticmethod
|
||||
def name_gen(path: Path) -> str:
|
||||
return sanitize(path.name).lower()
|
||||
|
||||
|
||||
# i meant plain
|
||||
def render_pain(ass: RsAsset) -> str:
|
||||
"""
|
||||
fym i have to deal with all RsAsset subclasses.
|
||||
|
||||
ts like a matrix of ahh.
|
||||
"""
|
||||
match ass:
|
||||
case RsAssetRoot(path):
|
||||
body = RsAssetModule.process_body(path, render_pain)
|
||||
return RsAssetRoot.plain_template(body)
|
||||
case RsAssetModule(path, name):
|
||||
body = RsAssetModule.process_body(path, render_pain)
|
||||
return RsAssetModule.plain_template(name, body)
|
||||
case RsAssetFile(path, name) as file:
|
||||
return RsAssetFile.plain_template(name, file.path_relative_to(src))
|
||||
case _:
|
||||
raise TypeError("who are you?")
|
||||
|
||||
|
||||
def render_macro(ass: RsAsset) -> str:
|
||||
"""
|
||||
ts is ridiculous
|
||||
"""
|
||||
match ass:
|
||||
case RsAssetRoot(path):
|
||||
body = RsAssetModule.process_body(path, render_macro)
|
||||
return RsAssetRoot.macroed_template(body)
|
||||
case RsAssetModule(path, name):
|
||||
body = RsAssetModule.process_body(path, render_macro)
|
||||
return RsAssetModule.macroed_template(name, body)
|
||||
case RsAssetFile(path, name) as file:
|
||||
return RsAssetFile.macroed_template(name, file.path_relative_to(src))
|
||||
case _:
|
||||
raise TypeError("who are you?")
|
||||
|
||||
|
||||
# region shit ass
|
||||
# im deleting this code because its so ass
|
||||
# endregion
|
||||
|
||||
lda_macro = """
|
||||
macro_rules! hehe {
|
||||
// Folder
|
||||
(folder $name:ident { $($children:tt)* } $($rest:tt)*) => {
|
||||
pub mod $name {
|
||||
hehe!{$($children)*}
|
||||
}
|
||||
hehe!{$($rest)*}
|
||||
};
|
||||
// File
|
||||
(file $name:ident $file:literal $($rest:tt)*) => {
|
||||
pub const $name: &str = include_str!($file);
|
||||
hehe!{$($rest)*}
|
||||
};
|
||||
// Base case
|
||||
() => {};
|
||||
}
|
||||
"""
|
||||
header = "// generated with ./scripts/map_static.py"
|
||||
|
||||
|
||||
class RsAssetRoot(RsAsset):
|
||||
__match_args__ = ("path",)
|
||||
|
||||
@staticmethod
|
||||
def plain_template(body: str):
|
||||
return f"""{header}
|
||||
|
||||
{body}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def macroed_template(body: str):
|
||||
indented_body = indent(body, 4)
|
||||
return f"""{header}
|
||||
{lda_macro}
|
||||
hehe! {{
|
||||
{indented_body}
|
||||
}}
|
||||
"""
|
||||
|
||||
def __init__(self, path: Path) -> None:
|
||||
os.scandir(path) # is you a dir?
|
||||
self.path = path
|
||||
|
||||
def __str__(self):
|
||||
const_name = sanitize(self.path.name).upper()
|
||||
return (
|
||||
f"pub const {const_name}: &str = "
|
||||
f'include_str!("{self.path.relative_to(out_rs.parent, walk_up=True).as_posix()}");'
|
||||
)
|
||||
# specify walk_up to have .. in yo path
|
||||
|
||||
asset_root = RsAssetRoot(static)
|
||||
|
||||
class RsAssetModule:
|
||||
def __init__(self, folder: Path, body_only: bool = False):
|
||||
self.folder = folder
|
||||
self.full = not body_only
|
||||
|
||||
def __str__(self):
|
||||
files = [str(RsAssetFile(s)) for s in self.folder.iterdir() if s.is_file()]
|
||||
subs = [str(RsAssetModule(s)) for s in self.folder.iterdir() if s.is_dir()]
|
||||
|
||||
body = "\n".join(files + subs)
|
||||
return (
|
||||
f"pub mod {sanitize(self.folder.name).lower()} {{" * self.full
|
||||
+ f"\n{indent(body, 4*self.full)}\n"
|
||||
+ self.full * "}"
|
||||
)
|
||||
# print(render_pain(asset_root))
|
||||
# print(render_macro(asset_root))
|
||||
|
||||
# rs_code = render_pain(asset_root)
|
||||
rs_code = render_macro(asset_root)
|
||||
|
||||
# generate
|
||||
rs_code = "// generated with ./scripts/map_static.py\n" + str(
|
||||
RsAssetModule(static, True)
|
||||
)
|
||||
out_rs.write_text(rs_code, encoding="utf-8")
|
||||
print(f"written to {out_rs}")
|
||||
print(f"written to {out_rs.relative_to(Path.cwd(), walk_up=True)}")
|
||||
@@ -10,32 +10,32 @@ INIT="/etc/init.d/wakey"
|
||||
KILL="/root/.bin/kill_wakey.sh"
|
||||
|
||||
if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then
|
||||
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
|
||||
exit 2
|
||||
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
[ -f "$BIN_TMP" ] || {
|
||||
echo "tmp binary not found: $BIN_TMP" >&2
|
||||
exit 1
|
||||
echo "tmp binary not found: $BIN_TMP" >&2
|
||||
exit 1
|
||||
}
|
||||
chmod +x "$BIN_TMP" || true
|
||||
|
||||
if [ "$RESTART" = "1" ]; then
|
||||
if [ -x "$INIT" ]; then
|
||||
"$INIT" stop || true
|
||||
elif [ -x "$KILL" ]; then
|
||||
sh "$KILL" || true
|
||||
fi
|
||||
if [ -x "$INIT" ]; then
|
||||
"$INIT" stop || true
|
||||
elif [ -x "$KILL" ]; then
|
||||
sh "$KILL" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
mv -f "$BIN_TMP" "$DEST"
|
||||
|
||||
if [ "$RESTART" = "1" ]; then
|
||||
if [ -x "$INIT" ]; then
|
||||
"$INIT" start || "$INIT" restart || true
|
||||
else
|
||||
nohup "$DEST" >/dev/null 2>&1 &
|
||||
fi
|
||||
if [ -x "$INIT" ]; then
|
||||
"$INIT" start || "$INIT" restart || true
|
||||
else
|
||||
nohup "$DEST" >/dev/null 2>&1 &
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Test ipjs on remote ARM device
|
||||
|
||||
param(
|
||||
[string]$Package = "lda-ipjs",
|
||||
[string]$TestName = "",
|
||||
[ValidateSet("debug", "release")]
|
||||
[string]$BuildProfile = "debug",
|
||||
[string]$password,
|
||||
[switch]$Verbose,
|
||||
[string]$RemoteTestPath = "/root/.bin/test",
|
||||
[string]$RemoteHost = "[email protected]"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Build tests and capture output
|
||||
Write-Host "Building tests for $Package..." -ForegroundColor Cyan
|
||||
|
||||
# Stream cargo output anc convert to text
|
||||
$cargoOutput = cargo test --no-run -p $Package --target armv7-unknown-linux-musleabihf $(if ($BuildProfile -eq "release") { "-r" }) 2>&1 |
|
||||
ForEach-Object {
|
||||
$line = if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { $_ }
|
||||
if ($Verbose) {
|
||||
Write-Host $line
|
||||
}
|
||||
$line # Pass through to capture
|
||||
}
|
||||
|
||||
# Parse test binary paths from cargo output
|
||||
$testBinaries = $cargoOutput |
|
||||
Select-String -Pattern "Executable.*\((.+)\)" |
|
||||
ForEach-Object { $_.Matches.Groups[1].Value } |
|
||||
Get-Item
|
||||
|
||||
if ($testBinaries.Count -eq 0) {
|
||||
Write-Error "No test binaries found!"
|
||||
Write-Host "Cargo output:" -ForegroundColor Yellow
|
||||
$cargoOutput | ForEach-Object { Write-Host $_ }
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Found $($testBinaries.Count) test $($testBinaries.Count -eq 1 ? "binary" : "binaries")" -ForegroundColor Green
|
||||
|
||||
# Run each test binary
|
||||
foreach ($testBinary in $testBinaries) {
|
||||
Write-Host "`nTesting: $($testBinary.Name)" -ForegroundColor Cyan
|
||||
|
||||
# Copy to target
|
||||
pscp.exe -batch -scp -pw $password $testBinary.FullName ${RemoteHost}:$RemoteTestPath | Out-Null
|
||||
|
||||
# Run on target
|
||||
$testArgs = "$(if ($Verbose) {"--nocapture --show-output"})"
|
||||
if ($TestName) {
|
||||
$testArgs = "$TestName $testArgs"
|
||||
}
|
||||
|
||||
plink -batch -ssh $RemoteHost -pw $password "chmod +x $RemoteTestPath && $RemoteTestPath $testArgs"
|
||||
}
|
||||
|
||||
Write-Host "`nDone!" -ForegroundColor Green
|
||||
+52
-2
@@ -15,5 +15,55 @@ impl<'de> Deserialize<'de> for NUDState {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use crate::utils::parse::mac::{des_opm, ser_opm};
|
||||
use crate::arpparse::IpNeighLine;
|
||||
|
||||
use lda_ipjs::subcommands::neighbor::{self as ipjs_neigh, NeighborItem};
|
||||
|
||||
impl From<ipjs_neigh::NUDState> for NUDState {
|
||||
fn from(value: ipjs_neigh::NUDState) -> Self {
|
||||
match value {
|
||||
ipjs_neigh::NUDState::Permanent => NUDState::Permanent,
|
||||
ipjs_neigh::NUDState::Noarp => NUDState::Noarp,
|
||||
ipjs_neigh::NUDState::Reachable => NUDState::Reachable,
|
||||
ipjs_neigh::NUDState::Stale => NUDState::Stale,
|
||||
ipjs_neigh::NUDState::None => NUDState::None,
|
||||
ipjs_neigh::NUDState::Incomplete => NUDState::Incomplete,
|
||||
ipjs_neigh::NUDState::Delay => NUDState::Delay,
|
||||
ipjs_neigh::NUDState::Probe => NUDState::Probe,
|
||||
ipjs_neigh::NUDState::Failed => NUDState::Failed,
|
||||
ipjs_neigh::NUDState::Other(_) => NUDState::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NUDState> for ipjs_neigh::NUDState {
|
||||
fn from(value: NUDState) -> Self {
|
||||
match value {
|
||||
NUDState::Permanent => ipjs_neigh::NUDState::Permanent,
|
||||
NUDState::Noarp => ipjs_neigh::NUDState::Noarp,
|
||||
NUDState::Reachable => ipjs_neigh::NUDState::Reachable,
|
||||
NUDState::Stale => ipjs_neigh::NUDState::Stale,
|
||||
NUDState::None => ipjs_neigh::NUDState::None,
|
||||
NUDState::Incomplete => ipjs_neigh::NUDState::Incomplete,
|
||||
NUDState::Delay => ipjs_neigh::NUDState::Delay,
|
||||
NUDState::Probe => ipjs_neigh::NUDState::Probe,
|
||||
NUDState::Failed => ipjs_neigh::NUDState::Failed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NeighborItem> for IpNeighLine {
|
||||
fn from(item: NeighborItem) -> Self {
|
||||
IpNeighLine {
|
||||
ip: item.ip,
|
||||
dev: Some(item.dev),
|
||||
mac: item.mac,
|
||||
state: item
|
||||
.state
|
||||
.first()
|
||||
.copied()
|
||||
.map(Into::into)
|
||||
.unwrap_or(NUDState::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
use std::{net::IpAddr, str::FromStr};
|
||||
|
||||
use impls::ser_opm;
|
||||
use crate::utils::parse::mac;
|
||||
use macaddr::MacAddr;
|
||||
use serde_with::skip_serializing_none;
|
||||
use strum::{Display, EnumString};
|
||||
@@ -33,7 +33,7 @@ pub struct IpNeighLine {
|
||||
pub ip: IpAddr,
|
||||
pub dev: Option<String>,
|
||||
/// link layer address
|
||||
#[serde(serialize_with = "ser_opm")]
|
||||
#[serde(with = "mac::option_mac")]
|
||||
pub mac: Option<MacAddr>,
|
||||
/// Neighbour Unreachability Detection
|
||||
pub state: NUDState,
|
||||
@@ -41,7 +41,9 @@ pub struct IpNeighLine {
|
||||
|
||||
// NUDState custom Deserialize now lives in arpparse/impl.rs; use serde_with OneOrMany for Vec
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize)]
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize, Default,
|
||||
)]
|
||||
#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum NUDState {
|
||||
@@ -63,10 +65,6 @@ pub enum NUDState {
|
||||
/// neighbour state if it was valid and the
|
||||
/// address is not changed by this command.
|
||||
Stale,
|
||||
/// this is a pseudo state used when initially
|
||||
/// creating a neighbour entry or after trying to
|
||||
/// remove it before it becomes free to do so.
|
||||
None,
|
||||
|
||||
/// the neighbour entry has not (yet) been
|
||||
/// validated/resolved.
|
||||
@@ -80,11 +78,18 @@ pub enum NUDState {
|
||||
/// success, neighbor validation has ultimately
|
||||
/// failed.
|
||||
Failed,
|
||||
|
||||
/// this is a pseudo state used when initially
|
||||
/// creating a neighbour entry or after trying to
|
||||
/// remove it before it becomes free to do so.
|
||||
#[serde(other)]
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl NUDState {
|
||||
/// Argument form expected by `ip neigh ... nud <state>` (lowercase)
|
||||
pub const fn as_ip_neigh_arg(self) -> &'static str {
|
||||
pub const fn _as_ip_neigh_arg(self) -> &'static str {
|
||||
match self {
|
||||
NUDState::Permanent => "permanent",
|
||||
NUDState::Reachable => "reachable",
|
||||
@@ -178,7 +183,7 @@ impl IpNeighLine {
|
||||
Self { state, ..self }
|
||||
}
|
||||
*/
|
||||
pub fn with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
|
||||
pub fn _with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
|
||||
let dev = dev.into();
|
||||
move |self_| Self {
|
||||
dev: Some(dev.clone()),
|
||||
+28
-9
@@ -1,12 +1,31 @@
|
||||
// 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");
|
||||
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
|
||||
() => {};
|
||||
}
|
||||
|
||||
hehe! {
|
||||
file HOME_2_HTML "../static/home_2.html"
|
||||
folder home_2 {
|
||||
file DOM_JS "../static/home_2/dom.js"
|
||||
file LEASES_JS "../static/home_2/leases.js"
|
||||
file MAIN_JS "../static/home_2/main.js"
|
||||
file STATUS_JS "../static/home_2/status.js"
|
||||
file STYLES_CSS "../static/home_2/styles.css"
|
||||
file UTILS_JS "../static/home_2/utils.js"
|
||||
file WAKE_JS "../static/home_2/wake.js"
|
||||
}
|
||||
}
|
||||
+4
-7
@@ -2,7 +2,8 @@
|
||||
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
|
||||
|
||||
/// Load MAC->name cache from disk
|
||||
async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> {
|
||||
pub(crate) async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>>
|
||||
{
|
||||
match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
|
||||
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()),
|
||||
@@ -41,8 +42,8 @@ pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLeaseLine>> {
|
||||
}
|
||||
Ok(leases_with_names)
|
||||
}
|
||||
use crate::utils::parse::mac;
|
||||
use macaddr::MacAddr;
|
||||
use serde::Serializer;
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::net::IpAddr;
|
||||
|
||||
@@ -52,15 +53,11 @@ pub struct DhcpLeaseLine {
|
||||
/// Epoch seconds when the lease expires
|
||||
pub expires_epoch: u64,
|
||||
pub ip: IpAddr,
|
||||
#[serde(serialize_with = "ser_mac")]
|
||||
#[serde(with = "mac")]
|
||||
pub mac: MacAddr,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
fn ser_mac<S: Serializer>(m: &MacAddr, s: S) -> Result<S::Ok, S::Error> {
|
||||
s.serialize_str(&m.to_string())
|
||||
}
|
||||
|
||||
/// Parse one line of /tmp/dhcp.leases
|
||||
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLeaseLine> {
|
||||
let mut c = line.split_whitespace();
|
||||
|
||||
+13
-8
@@ -1,7 +1,14 @@
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, post},
|
||||
};
|
||||
//! braindead version v0.1.x
|
||||
//!
|
||||
//! # whats next
|
||||
//!
|
||||
//! for version 2 i hope to have:
|
||||
//!
|
||||
//! 1. idk reworked frontend;
|
||||
//! 2. incorporate ip -j;
|
||||
//! 3. small 1-5 second caching;
|
||||
|
||||
use axum::{Router, routing::get};
|
||||
use tokio::net::TcpListener;
|
||||
mod arpparse;
|
||||
pub mod assets;
|
||||
@@ -10,18 +17,15 @@ mod route;
|
||||
mod utils;
|
||||
use std::io;
|
||||
|
||||
const MACHINE_NAME: &str = "lda.lan";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::main]
|
||||
async fn entry() -> io::Result<()> {
|
||||
use crate::route::{api_router, home_2, home_2_route, wake_handler};
|
||||
use crate::route::{api_router, home_2, home_2_route};
|
||||
|
||||
let app = Router::new()
|
||||
// .route("/home", get(home))
|
||||
.route("/", get(home_2))
|
||||
.merge(home_2_route())
|
||||
.route("/wake", post(wake_handler))
|
||||
// .route("/status", get(get_status_2))
|
||||
.nest("/api", api_router());
|
||||
|
||||
@@ -32,6 +36,7 @@ async fn entry() -> io::Result<()> {
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
use std::net::ToSocketAddrs;
|
||||
color_eyre::install()?;
|
||||
// use crate::arpparse::NUDState;
|
||||
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
|
||||
|
||||
@@ -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
@@ -1,32 +1,61 @@
|
||||
use crate::route::error::ApiError;
|
||||
use crate::utils::query_parser::{QueryType, parse_query};
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::post;
|
||||
use axum::{Router, extract::Path, response::Redirect, routing::get};
|
||||
use axum::{extract::Path, response::Redirect};
|
||||
|
||||
use crate::utils::route;
|
||||
use crate::route::status::{DeviceQuery, Filters, NamePath};
|
||||
use crate::utils::query::get_ips;
|
||||
|
||||
// Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly
|
||||
pub async fn status_smart_redirect(
|
||||
Path(q): Path<String>,
|
||||
) -> axum::response::Result<Redirect, impl IntoResponse> {
|
||||
match serde_html_form::to_string(route::status_smart_redirect(q).await) {
|
||||
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
|
||||
Err(e) => Err((
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(StatusError {
|
||||
error: e.to_string(),
|
||||
// no less bullshit
|
||||
let query = match parse_query(q).await {
|
||||
QueryType::Ip(ip_addr) => DeviceQuery {
|
||||
filter: Filters {
|
||||
ips: vec![ip_addr],
|
||||
..Default::default()
|
||||
}),
|
||||
)),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
QueryType::Mac(mac_addr) => DeviceQuery {
|
||||
filter: Filters {
|
||||
macs: vec![mac_addr],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
QueryType::Dev(s) => DeviceQuery {
|
||||
filter: Filters {
|
||||
devs: vec![s],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
QueryType::Nud(nudstate) => DeviceQuery {
|
||||
filter: Filters {
|
||||
nuds: vec![nudstate],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
QueryType::Unknown(n) => DeviceQuery {
|
||||
name: Some(n),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
match serde_html_form::to_string(query) {
|
||||
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
|
||||
Err(e) => Err(ApiError {
|
||||
error: e.to_string(),
|
||||
code: StatusCode::BAD_GATEWAY,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub use crate::route::devs::*;
|
||||
pub use crate::route::dhcp::*;
|
||||
pub use crate::route::status::*;
|
||||
pub use crate::route::wake::*;
|
||||
|
||||
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
|
||||
Redirect::permanent(&format!(
|
||||
"/api/status?name={name}",
|
||||
@@ -34,12 +63,9 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
|
||||
))
|
||||
}
|
||||
|
||||
pub fn api_router() -> Router {
|
||||
Router::new()
|
||||
.route("/status/{name}", get(status_redirect))
|
||||
.route("/status", get(get_status_json))
|
||||
.route("/dhcp_leases", get(get_dhcp_leases))
|
||||
.route("/smart/{q}", get(status_smart_redirect))
|
||||
.route("/devs", get(devs_router))
|
||||
.route("/wake", post(wake_multi))
|
||||
pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
|
||||
get_ips(&name).await.map_or_else(
|
||||
|e| ApiError::ise(e.to_string()).into_response(),
|
||||
|ips| Json(ips.collect::<Vec<_>>()).into_response(),
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -2,6 +2,6 @@ use crate::utils::query::dev;
|
||||
use axum::Json;
|
||||
|
||||
pub async fn devs_router() -> Json<Vec<String>> {
|
||||
dev::devs_sorted().into()
|
||||
dev::devs_sorted().await.into()
|
||||
}
|
||||
// Device listing endpoints
|
||||
+12
-11
@@ -1,4 +1,8 @@
|
||||
use crate::{dhcpparse, route::api::StatusError, utils::parse::boolish_str};
|
||||
use crate::{
|
||||
dhcpparse::read_dhcp_leases_with_names,
|
||||
route::error::ApiError,
|
||||
utils::{parse::boolish_str, query::enrich_leases_with_nud_state},
|
||||
};
|
||||
use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
|
||||
|
||||
// DHCP lease endpoints
|
||||
@@ -14,21 +18,18 @@ pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl Into
|
||||
.map(boolish_str)
|
||||
.unwrap_or(false);
|
||||
|
||||
match dhcpparse::read_dhcp_leases_with_names().await {
|
||||
match read_dhcp_leases_with_names().await {
|
||||
Ok(leases_with_names) => {
|
||||
if !include_state {
|
||||
return (StatusCode::OK, Json(leases_with_names)).into_response();
|
||||
}
|
||||
let out = crate::utils::query::enrich_leases_with_nud_state(leases_with_names).await;
|
||||
let out = enrich_leases_with_nud_state(leases_with_names).await;
|
||||
(StatusCode::OK, Json(out)).into_response()
|
||||
}
|
||||
Err(e) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(StatusError {
|
||||
error: e.to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => ApiError {
|
||||
error: e.to_string(),
|
||||
code: StatusCode::BAD_GATEWAY,
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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::response::IntoResponse;
|
||||
use axum::routing::post;
|
||||
use axum::{Router, http::header, response::Html, routing::get};
|
||||
|
||||
use crate::utils::route::serve_js;
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
+48
-94
@@ -1,33 +1,20 @@
|
||||
use crate::{
|
||||
arpparse::NUDState,
|
||||
utils::{
|
||||
parse::{de_many, serialize_macs},
|
||||
query::get_macs,
|
||||
},
|
||||
};
|
||||
use crate::route::error::ApiError;
|
||||
use axum::{Json, http::StatusCode, response::IntoResponse};
|
||||
use axum_extra::extract::Query;
|
||||
use macaddr::MacAddr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::collections::HashSet;
|
||||
use serde_with::{DisplayFromStr, OneOrMany, serde_as};
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::arpparse::NUDState;
|
||||
use crate::utils::query::get_macs;
|
||||
|
||||
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
|
||||
pub struct DeviceQuery {
|
||||
pub name: Option<String>,
|
||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
||||
pub ip: Vec<IpAddr>,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "de_many::vec_from_strs",
|
||||
serialize_with = "serialize_macs"
|
||||
)]
|
||||
pub mac: Vec<MacAddr>,
|
||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
||||
pub dev: Vec<String>,
|
||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
||||
pub nud: Vec<NUDState>,
|
||||
#[serde(flatten)]
|
||||
pub filter: Filters,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Hash, Deserialize)]
|
||||
@@ -43,82 +30,52 @@ pub struct Status<T> {
|
||||
pub filters: Filters,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
#[serde_as]
|
||||
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
|
||||
pub struct Filters {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub ip: Vec<IpAddr>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub dev: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub nud: Vec<NUDState>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Vec::is_empty",
|
||||
serialize_with = "serialize_macs"
|
||||
)]
|
||||
pub mac: Vec<MacAddr>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Default)]
|
||||
pub struct StatusError {
|
||||
pub name: Option<String>,
|
||||
pub error: String,
|
||||
#[serde_as(as = "OneOrMany<_>")]
|
||||
#[serde(default)]
|
||||
pub ips: Vec<IpAddr>,
|
||||
#[serde_as(as = "OneOrMany<_>")]
|
||||
#[serde(default)]
|
||||
pub devs: Vec<String>,
|
||||
#[serde_as(as = "OneOrMany<_>")]
|
||||
#[serde(default)]
|
||||
pub nuds: Vec<NUDState>,
|
||||
#[serde_as(as = "OneOrMany<DisplayFromStr>")]
|
||||
#[serde(default)]
|
||||
pub macs: Vec<MacAddr>,
|
||||
}
|
||||
|
||||
pub async fn get_status_json(
|
||||
Query(DeviceQuery {
|
||||
name,
|
||||
ip,
|
||||
dev,
|
||||
nud,
|
||||
mac,
|
||||
filter:
|
||||
Filters {
|
||||
ips,
|
||||
devs,
|
||||
nuds,
|
||||
macs,
|
||||
},
|
||||
..
|
||||
}): Query<DeviceQuery>,
|
||||
) -> impl IntoResponse {
|
||||
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> {
|
||||
if slice.is_empty() {
|
||||
vec![None]
|
||||
} else {
|
||||
slice.iter().cloned().map(Some).collect()
|
||||
}
|
||||
}
|
||||
let ips_opt = if ip.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(ip.clone())
|
||||
};
|
||||
let dev_opts: Vec<Option<String>> = to_opts(&dev);
|
||||
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
|
||||
let filters = Filters {
|
||||
ip,
|
||||
dev,
|
||||
nud,
|
||||
mac: mac.clone(),
|
||||
};
|
||||
let mut tasks = Vec::new();
|
||||
// can we jus collect the whole table instead of doing this.
|
||||
for d in &dev_opts {
|
||||
for n in &nud_opts {
|
||||
tasks.push(get_macs(
|
||||
// this i hate sm
|
||||
name.as_deref(),
|
||||
ips_opt.as_deref(),
|
||||
d.as_deref(),
|
||||
*n,
|
||||
));
|
||||
}
|
||||
}
|
||||
// why try join all?
|
||||
match futures::future::try_join_all(tasks)
|
||||
.await
|
||||
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
|
||||
match get_macs(
|
||||
&name.iter().collect::<Vec<_>>(),
|
||||
&ips,
|
||||
&devs.iter().collect::<Vec<_>>(),
|
||||
&nuds,
|
||||
&macs,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(mut table) => {
|
||||
if !mac.is_empty() {
|
||||
// mac here
|
||||
let wanted: HashSet<MacAddr> = mac.into_iter().collect();
|
||||
table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false));
|
||||
}
|
||||
Ok(table) => {
|
||||
let filters = Filters {
|
||||
ips,
|
||||
devs,
|
||||
nuds,
|
||||
macs,
|
||||
};
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(Status {
|
||||
@@ -129,14 +86,11 @@ pub async fn get_status_json(
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(error) => (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(StatusError {
|
||||
name,
|
||||
error: error.to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => ApiError {
|
||||
code: StatusCode::BAD_GATEWAY,
|
||||
error: error.to_string(),
|
||||
}
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
// Status endpoints
|
||||
+14
-21
@@ -4,7 +4,7 @@ use std::net::IpAddr;
|
||||
|
||||
/* use crate::arpparse::IpNeighLine;
|
||||
use crate::route::api::Status; */
|
||||
use crate::utils::parse::mac::{des_opm, ser_opm};
|
||||
use crate::utils::parse::mac;
|
||||
use crate::utils::wake::wake_one;
|
||||
use axum::{extract::Json, http::StatusCode, response::IntoResponse};
|
||||
use futures::TryFutureExt;
|
||||
@@ -23,9 +23,8 @@ pub struct WakeResult {
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Clone, Copy)]
|
||||
pub struct WakeTargetResult {
|
||||
pub ip: Option<IpAddr>,
|
||||
#[serde(serialize_with = "ser_opm")]
|
||||
pub mac: Option<MacAddr>,
|
||||
#[serde(flatten)]
|
||||
pub target: WakeTarget,
|
||||
pub status: WakeTargetStatus,
|
||||
}
|
||||
|
||||
@@ -41,11 +40,11 @@ pub enum WakeTargetStatus {
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct WakeTarget {
|
||||
#[serde(default)]
|
||||
pub ip: Option<IpAddr>,
|
||||
#[serde(default, deserialize_with = "des_opm")]
|
||||
#[serde(default, with = "mac::option_mac")]
|
||||
pub mac: Option<MacAddr>,
|
||||
}
|
||||
|
||||
@@ -82,21 +81,15 @@ pub async fn wake_multi_split(
|
||||
.await?;
|
||||
sock.set_broadcast(true)?;
|
||||
|
||||
Ok(
|
||||
futures::future::join_all(targets.into_iter().map(async |c| {
|
||||
if c.is_incomplete() {
|
||||
c.to_incomplete()
|
||||
} else {
|
||||
wake_one(
|
||||
&sock,
|
||||
c.try_into().expect("complete struct failed to try_into"), // type state pattern?
|
||||
)
|
||||
.await
|
||||
.into()
|
||||
}
|
||||
}))
|
||||
.await,
|
||||
)
|
||||
let iter = targets.into_iter().map(async |c| {
|
||||
if c.is_incomplete() {
|
||||
c.to_incomplete()
|
||||
} else {
|
||||
let t = c.try_into().expect("complete struct failed to try_into");
|
||||
wake_one(&sock, t).await.into()
|
||||
}
|
||||
});
|
||||
Ok(futures::future::join_all(iter).await)
|
||||
}
|
||||
/* #[derive(Debug, Serialize)]
|
||||
pub struct WakeStatusLine {
|
||||
|
||||
@@ -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>
|
||||
"#,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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;
|
||||
pub mod query_parser;
|
||||
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;
|
||||
+19
-58
@@ -100,52 +100,12 @@ pub fn boolish_str(s: &str) -> bool {
|
||||
&& t.parse::<u64>().map(|n| n != 0).unwrap_or(false))
|
||||
}
|
||||
|
||||
pub mod de_many {
|
||||
use serde::Deserialize;
|
||||
use serde::de;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum OneOrMany<T> {
|
||||
One(T),
|
||||
Many(Vec<T>),
|
||||
}
|
||||
|
||||
pub fn vec_from_strs<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
T: std::str::FromStr,
|
||||
T::Err: std::fmt::Display,
|
||||
{
|
||||
let raw: OneOrMany<String> = OneOrMany::<String>::deserialize(des)?;
|
||||
let mut out = Vec::new();
|
||||
match raw {
|
||||
OneOrMany::One(s) => {
|
||||
let t = s.trim();
|
||||
if !t.is_empty() {
|
||||
out.push(t.parse().map_err(de::Error::custom)?);
|
||||
}
|
||||
}
|
||||
OneOrMany::Many(vs) => {
|
||||
for s in vs {
|
||||
let t = s.trim();
|
||||
if t.is_empty() {
|
||||
continue;
|
||||
}
|
||||
out.push(t.parse().map_err(de::Error::custom)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod mac {
|
||||
use macaddr::MacAddr;
|
||||
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
|
||||
use serde::{Serialize, Serializer, de};
|
||||
|
||||
pub fn serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
|
||||
pub fn _serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
@@ -154,7 +114,7 @@ pub mod mac {
|
||||
}
|
||||
|
||||
/// Serialize a MacAddr as a string
|
||||
pub fn _serialize_mac<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
|
||||
pub fn serialize<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
@@ -162,7 +122,7 @@ pub mod mac {
|
||||
}
|
||||
|
||||
/// Deserialize a MacAddr from a string
|
||||
pub fn _deserialize_mac<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
|
||||
pub fn _deserialize<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
@@ -170,21 +130,22 @@ pub mod mac {
|
||||
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)
|
||||
}
|
||||
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 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)
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use mac::*;
|
||||
+2
-2
@@ -7,7 +7,7 @@ use tokio::{
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use crate::{arpparse::NUDState, utils::query::get_macs};
|
||||
use crate::{arpparse::NUDState, utils::query::get_mac};
|
||||
|
||||
pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
|
||||
timeout(Duration::from_secs(1), TcpStream::connect(addr))
|
||||
@@ -19,7 +19,7 @@ pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
|
||||
}
|
||||
|
||||
pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 {
|
||||
match get_macs(None, Some(&[addr.into()]), None, None).await {
|
||||
match get_mac(Some(addr.into()), None, &[] as &[NUDState]).await {
|
||||
Err(_) => 0,
|
||||
Ok(l) => l
|
||||
.into_iter()
|
||||
|
||||
+11
-32
@@ -1,45 +1,24 @@
|
||||
use std::{collections::HashSet, fs};
|
||||
use lda_ipjs::subcommands::address::json as ipjs_json;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn get_dev() -> HashSet<String> {
|
||||
pub async fn get_dev() -> HashSet<String> {
|
||||
let mut devs: HashSet<String> = HashSet::new();
|
||||
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
|
||||
for e in rd.flatten() {
|
||||
if e.file_type()
|
||||
.map(|ft| {
|
||||
if ft.is_symlink() {
|
||||
// true
|
||||
fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false)
|
||||
} else {
|
||||
ft.is_dir()
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
&& let Ok(name) = e.file_name().into_string()
|
||||
&& name != "lo"
|
||||
&& !name.is_empty()
|
||||
{
|
||||
devs.insert(name);
|
||||
}
|
||||
}
|
||||
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
|
||||
for line in txt.lines().skip(2) {
|
||||
if let Some((name, _rest)) = line.split_once(':') {
|
||||
let n = name.trim().to_string();
|
||||
if n != "lo" && !n.is_empty() {
|
||||
devs.insert(n);
|
||||
}
|
||||
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 devs_sorted() -> Vec<String> {
|
||||
let mut v: Vec<String> = get_dev().into_iter().collect();
|
||||
pub async fn devs_sorted() -> Vec<String> {
|
||||
let mut v: Vec<String> = get_dev().await.into_iter().collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
pub fn has_dev(name: &str) -> bool {
|
||||
get_dev().contains(name)
|
||||
pub async fn has_dev(name: &str) -> bool {
|
||||
get_dev().await.contains(name)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::arpparse::NUDState;
|
||||
use crate::dhcpparse::DhcpLeaseLine;
|
||||
use crate::utils::query::get_macs;
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::net::IpAddr;
|
||||
|
||||
@@ -14,11 +15,10 @@ pub struct DhcpLeaseOut {
|
||||
|
||||
/// Enrich DHCP leases with NUD state and rank using get_macs
|
||||
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
|
||||
use crate::utils::query::macs::get_macs;
|
||||
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
|
||||
let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> =
|
||||
std::collections::HashMap::new();
|
||||
if let Ok(rows) = get_macs(None, Some(&ips), None, None).await {
|
||||
if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await {
|
||||
for row in rows {
|
||||
let state = row.state;
|
||||
let r = state.rank();
|
||||
|
||||
+107
-118
@@ -1,139 +1,128 @@
|
||||
use crate::arpparse::{self, IpNeighLine, NUDState};
|
||||
use crate::utils::{
|
||||
cmd::exec_command,
|
||||
error::{self, Error, Result},
|
||||
};
|
||||
use macaddr::MacAddr;
|
||||
|
||||
use crate::arpparse::{IpNeighLine, NUDState};
|
||||
use anyhow::{Context, Result};
|
||||
use lda_ipjs::subcommands::neighbor as ipjs_neigh;
|
||||
use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub async fn get_ips(machine_name: &str) -> error::Result<impl Iterator<Item = IpAddr>> {
|
||||
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
||||
Ok(tokio::net::lookup_host((machine_name, 0))
|
||||
.await
|
||||
.map_err(|e| error::Error::DnsResolve {
|
||||
name: machine_name.to_string(),
|
||||
source: e,
|
||||
})?
|
||||
.with_context(|| format!("DNS resolve failed for {machine_name}"))?
|
||||
.map(|c| c.ip()))
|
||||
}
|
||||
|
||||
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
|
||||
Ok(get_macs_1(machine_name)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|IpNeighLine {
|
||||
ip,
|
||||
dev: _,
|
||||
mac,
|
||||
state,
|
||||
}| mac.map(|mac| (ip, mac, state)),
|
||||
)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> {
|
||||
Ok(get_macs_1(machine_name)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|IpNeighLine {
|
||||
ip: _,
|
||||
dev: _,
|
||||
mac,
|
||||
state: _,
|
||||
}| mac,
|
||||
)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_macs_1(machine_name: &str) -> Result<Vec<arpparse::IpNeighLine>> {
|
||||
let dev = "br-lan";
|
||||
let ips = get_ips(machine_name).await?;
|
||||
let futures = ips.map(|ip| {
|
||||
let ip = ip.to_canonical();
|
||||
async move {
|
||||
let cmd = "ip";
|
||||
let args = ["neigh", "show", "to", &ip.to_string(), "dev", dev];
|
||||
let o = exec_command(cmd, args).await?;
|
||||
if !o.status.success() {
|
||||
return Err(Error::CommandFailed {
|
||||
cmd,
|
||||
args: args.iter().map(ToString::to_string).collect(),
|
||||
status: o.status.code(),
|
||||
stderr: String::from_utf8_lossy(&o.stderr).into(),
|
||||
});
|
||||
};
|
||||
Ok(String::from_utf8_lossy(&o.stdout)
|
||||
.lines()
|
||||
.flat_map(arpparse::parse_ip_neigh_line)
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
});
|
||||
let res = futures::future::try_join_all(futures).await?;
|
||||
Ok(res.into_iter().flatten().collect())
|
||||
}
|
||||
|
||||
// good now
|
||||
//
|
||||
// Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field.
|
||||
// This is because missing dev/mac usually means the entry is incomplete/transient.
|
||||
//
|
||||
// when there is only one MACs (getmac got some), the result will not have them fields.
|
||||
// so there are three cases:
|
||||
//
|
||||
// 1. dont got nothing: take all of them (macset.is_empty())
|
||||
// 2. exactly one: pre-filtered by ip, everything matches,
|
||||
// devset.len() != 1 returns false, but then it works????
|
||||
// OH THIS fuckass code i added it in the get_mac
|
||||
// 3. devset.len() > 1. if none then absolutely not match,
|
||||
// if some then check with the set; thats normal
|
||||
pub async fn get_macs(
|
||||
machine_name: Option<&str>,
|
||||
ips: Option<&[IpAddr]>,
|
||||
dev: Option<&str>,
|
||||
state: Option<NUDState>,
|
||||
machine_names: &[impl AsRef<str>],
|
||||
ips: &[IpAddr],
|
||||
devs: &[impl AsRef<str>],
|
||||
state: &[NUDState],
|
||||
macs: &[MacAddr],
|
||||
) -> Result<Vec<IpNeighLine>> {
|
||||
let ip_list: Option<Vec<IpAddr>> =
|
||||
ips.map(|slice| slice.iter().map(|ip| ip.to_canonical()).collect());
|
||||
let ip_list = match (ip_list, machine_name) {
|
||||
(Some(list), _) => list,
|
||||
(None, Some(name)) => get_ips(name).await?.collect(),
|
||||
(None, None) => Vec::new(),
|
||||
};
|
||||
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, dev, state);
|
||||
if !ip_list.is_empty() {
|
||||
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip)));
|
||||
let res = futures::future::try_join_all(futures).await?;
|
||||
Ok(res.into_iter().flatten().collect())
|
||||
let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect();
|
||||
let ip_m: HashSet<IpAddr> =
|
||||
futures::future::try_join_all(machine_names.iter().map(|c| get_ips(c.as_ref())))
|
||||
.await?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
let ip_all = if ip_set.is_empty() && ip_m.is_empty() {
|
||||
None
|
||||
} else if ip_set.is_empty() {
|
||||
Some(ip_m)
|
||||
} else if ip_m.is_empty() {
|
||||
Some(ip_set)
|
||||
} else {
|
||||
run_one(None).await
|
||||
}
|
||||
Some({
|
||||
ip_set.retain(|c| ip_m.contains(c)); // inline AHHH
|
||||
ip_set
|
||||
})
|
||||
};
|
||||
|
||||
let opt_dev = if devs.len() > 1 {
|
||||
None
|
||||
} else {
|
||||
devs.iter().next().map(AsRef::as_ref)
|
||||
};
|
||||
|
||||
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, opt_dev, state);
|
||||
|
||||
let mut ip_filtered = if let Some(something) = ip_all {
|
||||
if something.len() == 1 {
|
||||
run_one(something.into_iter().next()).await?
|
||||
} else {
|
||||
run_one(None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|c| something.contains(&c.ip))
|
||||
.collect()
|
||||
}
|
||||
} else {
|
||||
run_one(None).await?
|
||||
};
|
||||
|
||||
// Apply additional filters if any were provided
|
||||
if !devs.is_empty() || !macs.is_empty() || !state.is_empty() {
|
||||
let devset: HashSet<_> = devs.iter().map(AsRef::as_ref).collect();
|
||||
let macset: HashSet<_> = macs.iter().collect();
|
||||
|
||||
ip_filtered.retain(|entry| {
|
||||
// Dev filter: if we're filtering by dev, entry must have a dev AND it must be in the set
|
||||
let dev_ok =
|
||||
devset.is_empty() || entry.dev.as_deref().is_some_and(|d| devset.contains(d));
|
||||
|
||||
// MAC filter: if we're filtering by MAC, entry must have a MAC AND it must be in the set
|
||||
let mac_ok = macset.is_empty() || entry.mac.is_some_and(|m| macset.contains(&m));
|
||||
|
||||
dev_ok && mac_ok
|
||||
})
|
||||
};
|
||||
Ok(ip_filtered)
|
||||
}
|
||||
|
||||
/// the atomic get_macs. handle ONE thing only.
|
||||
pub async fn get_mac(
|
||||
ip: Option<IpAddr>,
|
||||
dev: Option<&str>,
|
||||
state: Option<NUDState>,
|
||||
) -> error::Result<Vec<IpNeighLine>> {
|
||||
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
|
||||
if let Some(ip) = ip {
|
||||
args.push("to".into());
|
||||
args.push(ip.to_string());
|
||||
}
|
||||
if let Some(d) = dev {
|
||||
args.push("dev".into());
|
||||
args.push(d.to_string());
|
||||
}
|
||||
if let Some(nud) = state {
|
||||
args.push("nud".into());
|
||||
args.push(nud.as_ip_neigh_arg().into());
|
||||
}
|
||||
let cmd = "ip";
|
||||
let out = exec_command(cmd, args.iter().map(String::as_str).collect::<Vec<_>>()).await?;
|
||||
if !out.status.success() {
|
||||
return Err(error::Error::CommandFailed {
|
||||
cmd,
|
||||
args,
|
||||
status: out.status.code(),
|
||||
stderr: String::from_utf8_lossy(&out.stderr).into(),
|
||||
});
|
||||
}
|
||||
let lines = String::from_utf8_lossy(&out.stdout);
|
||||
let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line);
|
||||
let rows: Vec<IpNeighLine> = if let Some(d) = dev {
|
||||
parsed.map(IpNeighLine::with_dev(d)).collect()
|
||||
} else {
|
||||
parsed.collect()
|
||||
};
|
||||
Ok(rows)
|
||||
state: &[NUDState],
|
||||
) -> Result<Vec<IpNeighLine>> {
|
||||
let ipjs_states: Vec<ipjs_neigh::NUDState> = state.iter().copied().map(Into::into).collect();
|
||||
|
||||
let items = ipjs_neigh::json::get(ip, dev, &ipjs_states)
|
||||
.await
|
||||
.context("Calling ip -j neigh failed")?;
|
||||
|
||||
let lines = items
|
||||
.into_iter()
|
||||
.map(|item| IpNeighLine {
|
||||
ip: item.ip,
|
||||
dev: Some(item.dev),
|
||||
mac: item.mac,
|
||||
state: item
|
||||
.state
|
||||
.first()
|
||||
.copied()
|
||||
.map(Into::into)
|
||||
.unwrap_or(NUDState::None),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
File renamed without changes.
@@ -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())
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
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 {
|
||||
(
|
||||
@@ -14,59 +9,3 @@ pub async fn serve_js(content: &'static str) -> impl IntoResponse {
|
||||
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()
|
||||
}
|
||||
}
|
||||
+13
-6
@@ -22,25 +22,32 @@ impl TryFrom<RouteWakeTarget> for WakeTarget {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WakeTargetResult> for RouteWakeResult {
|
||||
fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> Self {
|
||||
impl From<WakeTarget> for RouteWakeTarget {
|
||||
fn from(WakeTarget { ip, mac }: WakeTarget) -> Self {
|
||||
Self {
|
||||
ip: Some(ip),
|
||||
mac: Some(mac),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WakeTargetResult> for RouteWakeResult {
|
||||
fn from(WakeTargetResult { target, status }: WakeTargetResult) -> Self {
|
||||
Self {
|
||||
target: target.into(),
|
||||
status: status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RouteWakeTarget {
|
||||
pub fn to_incomplete(self) -> RouteWakeResult {
|
||||
pub const fn to_incomplete(self) -> RouteWakeResult {
|
||||
RouteWakeResult {
|
||||
ip: self.ip,
|
||||
mac: self.mac,
|
||||
target: self,
|
||||
status: RouteWakeStatus::Incomplete,
|
||||
}
|
||||
}
|
||||
pub fn is_incomplete(&self) -> bool {
|
||||
pub const fn is_incomplete(&self) -> bool {
|
||||
!matches!(
|
||||
self,
|
||||
Self {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//! why did my Head Ass split these into two.
|
||||
|
||||
pub mod impls;
|
||||
use std::{io, net::IpAddr};
|
||||
|
||||
@@ -5,35 +7,6 @@ use futures::TryFutureExt;
|
||||
use macaddr::MacAddr;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
use crate::utils::query::get_macs_2_mac;
|
||||
|
||||
pub async fn wake(machine_name: &str) -> io::Result<u32> {
|
||||
let suh = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
suh.set_broadcast(true)?;
|
||||
let /* mut */ macs = get_macs_2_mac(machine_name).await.unwrap_or_default();
|
||||
// macs.extend(*LDA_MACS_2);
|
||||
let mut sent_ok = 0;
|
||||
for mac in macs {
|
||||
let mb = mac.as_bytes();
|
||||
|
||||
let mut pac = [0; 6 + 6 * 16]; // 6x FF + 16x mac6
|
||||
pac[..6].fill(0xff);
|
||||
for i in 1..=16 {
|
||||
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
|
||||
}
|
||||
|
||||
match suh
|
||||
.send_to(&pac, (IpAddr::from([192, 168, 100, 255]), 9))
|
||||
.await
|
||||
{
|
||||
Ok(n) if n == pac.len() => sent_ok += 1,
|
||||
Ok(n) => eprintln!("partial send ({n}/{})", pac.len()),
|
||||
Err(e) => eprintln!("send error: {e}"),
|
||||
}
|
||||
}
|
||||
Ok(sent_ok)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash)]
|
||||
pub struct WakeTarget {
|
||||
pub ip: IpAddr,
|
||||
@@ -41,8 +14,7 @@ pub struct WakeTarget {
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Hash)]
|
||||
pub struct WakeTargetResult {
|
||||
pub ip: IpAddr,
|
||||
pub mac: MacAddr,
|
||||
pub target: WakeTarget,
|
||||
pub status: WakeStatus,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, Hash)]
|
||||
@@ -52,22 +24,22 @@ pub enum WakeStatus {
|
||||
WrongSize,
|
||||
}
|
||||
impl WakeTarget {
|
||||
fn _new(ip: IpAddr, mac: MacAddr) -> Self {
|
||||
const fn _new(ip: IpAddr, mac: MacAddr) -> Self {
|
||||
Self { ip, mac }
|
||||
}
|
||||
fn good(self) -> WakeTargetResult {
|
||||
WakeTargetResult::new(self.ip, self.mac, WakeStatus::Success)
|
||||
const fn good(self) -> WakeTargetResult {
|
||||
WakeTargetResult::new(self, WakeStatus::Success)
|
||||
}
|
||||
fn bad(self) -> WakeTargetResult {
|
||||
WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize)
|
||||
const fn bad(self) -> WakeTargetResult {
|
||||
WakeTargetResult::new(self, WakeStatus::WrongSize)
|
||||
}
|
||||
fn errored(self) -> WakeTargetResult {
|
||||
WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress)
|
||||
const fn errored(self) -> WakeTargetResult {
|
||||
WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
|
||||
}
|
||||
}
|
||||
impl WakeTargetResult {
|
||||
fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self {
|
||||
Self { ip, mac, status }
|
||||
const fn new(target: WakeTarget, status: WakeStatus) -> Self {
|
||||
Self { target, status }
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
<div class="row muted" style="gap: 16px">
|
||||
<span
|
||||
>Uses query
|
||||
<span title="available keys: name, ip, mac, dev, nud"
|
||||
<span title="available keys: name, ips, macs, devs, nuds"
|
||||
>(?name=...)</span
|
||||
>
|
||||
on this page to view the status.<!-- and header (X-Target-Name) so either extractor
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { elLeases } from "./dom.js";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* expires_epoch: Number
|
||||
* rank?: Number
|
||||
* ip: String
|
||||
* mac: String
|
||||
* nud_state?: String
|
||||
* name?: String
|
||||
* }[]} leases
|
||||
* @returns
|
||||
*/
|
||||
export function renderLeases(leases) {
|
||||
if (!elLeases) return;
|
||||
if (!Array.isArray(leases) || leases.length === 0) {
|
||||
|
||||
+89
-3
@@ -2,6 +2,7 @@ import { elHtml, elLog, setPill, qs, pill } from "./dom.js";
|
||||
import { rankState } from "./utils.js";
|
||||
import { merge_wake_data, translate_wake_message } from "./wake.js";
|
||||
|
||||
// IpNeighLine
|
||||
const status_map = {
|
||||
ip: "ip",
|
||||
mac: "mac",
|
||||
@@ -9,8 +10,14 @@ const status_map = {
|
||||
dev: "interface",
|
||||
};
|
||||
export const status_array = Object.keys(status_map);
|
||||
export const filter_array = ["ip", "dev", "nud", "mac"];
|
||||
// Filters
|
||||
export const filter_array = ["ips", "devs", "nuds", "macs"];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} name
|
||||
* @returns {URL}
|
||||
*/
|
||||
function buildStatusUrl(name) {
|
||||
const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length);
|
||||
if (name && !hasExtraFilters) {
|
||||
@@ -25,6 +32,25 @@ function buildStatusUrl(name) {
|
||||
return u;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* has_wake?: true
|
||||
* table: {
|
||||
* wake_status?: Boolean
|
||||
* ips: String
|
||||
* dev: String
|
||||
* mac: String
|
||||
* state: String
|
||||
* }[]
|
||||
* filters: {
|
||||
* ips?: String[]
|
||||
* devs?: String[]
|
||||
* nuds?: String[]
|
||||
* macs?: String[]
|
||||
* }
|
||||
* }} data
|
||||
*/
|
||||
export function renderStatus(data) {
|
||||
const tbl = document.createElement("table");
|
||||
tbl.className = "table";
|
||||
@@ -82,12 +108,51 @@ export function renderStatus(data) {
|
||||
if (r >= 5) setPill("ok", "online");
|
||||
else if (r >= 2) setPill("warn", "maybe");
|
||||
else setPill("bad", "offline");
|
||||
if (data.filters.nud?.length > 0) pill.textContent += " (filtered)";
|
||||
if (data.filters.nuds?.length > 0) pill.textContent += " (filtered)";
|
||||
} else {
|
||||
setPill("warn", "unknown");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} name
|
||||
* @param {Boolean} render
|
||||
* @param {{
|
||||
* ip: String,
|
||||
* mac: String,
|
||||
* status:
|
||||
* "incomplete" | "succeed" | "nonexistent_address" | "wrong_size"
|
||||
* }} data_wake
|
||||
* @returns {{
|
||||
* has_wake: false
|
||||
* table: {
|
||||
* ip: String
|
||||
* dev: String
|
||||
* mac: String
|
||||
* state: String
|
||||
* }[]
|
||||
* filters: {
|
||||
* ips?: String[]
|
||||
* devs?: String[]
|
||||
* nuds?: String[]
|
||||
* macs?: String[]
|
||||
* }
|
||||
* } | {
|
||||
* has_wake: true
|
||||
* table: {
|
||||
* wake_status: Boolean
|
||||
* ips: String
|
||||
* dev: String
|
||||
* mac: String
|
||||
* state: String
|
||||
* }[]
|
||||
* filters: {
|
||||
* ips?: String[]
|
||||
* devs?: String[]
|
||||
* nuds?: String[]
|
||||
* macs?: String[]
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export async function fetchStatus(name, render = true, data_wake) {
|
||||
setPill("warn", "checking…");
|
||||
const u = buildStatusUrl(name);
|
||||
@@ -97,6 +162,7 @@ export async function fetchStatus(name, render = true, data_wake) {
|
||||
if (!r.ok) {
|
||||
let msg = String(r.status);
|
||||
try {
|
||||
/** @type {{error: string}} */
|
||||
const err = await r.clone().json();
|
||||
msg = err.error || JSON.stringify(err);
|
||||
} catch {
|
||||
@@ -106,6 +172,26 @@ export async function fetchStatus(name, render = true, data_wake) {
|
||||
setPill("bad", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* this one does not have data wake
|
||||
* @type {{
|
||||
* table: {
|
||||
* ip: String
|
||||
* dev: String
|
||||
* mac: String
|
||||
* state: String
|
||||
* wake_status?: String
|
||||
* }[]
|
||||
* filters: {
|
||||
* ips?: String[]
|
||||
* devs?: String[]
|
||||
* nuds?: String[]
|
||||
* macs?: String[]
|
||||
* }
|
||||
* has_wake?: true
|
||||
* }}
|
||||
*/
|
||||
const data = await r.json();
|
||||
|
||||
if (data_wake) {
|
||||
|
||||
@@ -15,6 +15,13 @@ body {
|
||||
padding: 0;
|
||||
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
wrap {
|
||||
margin-inline: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
min-height: 100dvh;
|
||||
|
||||
+31
-3
@@ -1,6 +1,9 @@
|
||||
import { elHtml, elLog, setPill } from "./dom.js";
|
||||
import { fetchStatus, renderStatus } from "./status.js";
|
||||
|
||||
/**
|
||||
* @param {String} name just plain name
|
||||
*/
|
||||
export async function sendWake(name) {
|
||||
const data = await fetchStatus(name, false);
|
||||
if (!data) return; // can not proceed; theres nothing.
|
||||
@@ -51,8 +54,29 @@ export async function sendWake(name) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array} table
|
||||
* @param {Array} wake
|
||||
* @param {{
|
||||
* ip: String
|
||||
* dev: String
|
||||
* mac: String
|
||||
* state: String
|
||||
* }[]} table
|
||||
* @param {{
|
||||
* ip: String,
|
||||
* mac: String,
|
||||
* status: "incomplete" | "succeed" | "nonexistent_address" | "wrong_size"
|
||||
* }[]} wake
|
||||
* @returns {{
|
||||
* ip: String
|
||||
* dev: String
|
||||
* mac: String
|
||||
* state: String
|
||||
* }[] | {
|
||||
* ip: String
|
||||
* dev: String
|
||||
* mac: String
|
||||
* state: String
|
||||
* wake_status: boolean
|
||||
* }[]}
|
||||
*/
|
||||
export function merge_wake_data(table, wake) {
|
||||
if (!wake) return table;
|
||||
@@ -90,7 +114,11 @@ export function merge_wake_data(table, wake) {
|
||||
return return_array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"incomplete" | "succeed" | "nonexistent_address" | "wrong_size" | any} wake_msg
|
||||
* @returns {string}
|
||||
*/
|
||||
export function translate_wake_message(wake_msg) {
|
||||
switch (wake_msg) {
|
||||
case "incomplete":
|
||||
|
||||
Reference in new issue
Block a user