Compare commits
73
Commits
v0.1.0
...
3339faebfb
No files matched your search
+7
-2
@@ -1,5 +1,10 @@
|
|||||||
|
[target.armv7-unknown-linux-musleabihf]
|
||||||
|
linker = "rust-lld"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
target = "armv7-unknown-linux-musleabihf"
|
target = "armv7-unknown-linux-musleabihf"
|
||||||
|
|
||||||
[target.armv7-unknown-linux-musleabihf]
|
[alias]
|
||||||
linker = "rust-lld"
|
ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
|
||||||
|
t = "test -- --nocapture --test-threads=1"
|
||||||
|
tdebug = "test -- --nocapture --test-threads=1 --show-output"
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
you can use block code as so:
|
you should use block code to refence code, like so:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
hello_world!(println);
|
hello_world!(println);
|
||||||
|
// indented lines
|
||||||
```
|
```
|
||||||
in the chat.
|
|
||||||
|
in the chat. Else it fucks up formatting.
|
||||||
|
|
||||||
|
We work towards clean, maintainable design. im not the best at this thing.
|
||||||
Vendored
+3
-2
@@ -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
+433
-228
File diff suppressed because it is too large.
Load diff
+35
-13
@@ -1,23 +1,45 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wakey"
|
name = "wakey"
|
||||||
version = "0.1.0"
|
version = "0.1.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
publish = ["gitea"]
|
publish = ["gitea"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.8.4", features = ["macros"] }
|
anyhow = "1"
|
||||||
axum-extra = { version = "0.10.1", features = ["query"] }
|
axum = { version = "0", features = ["macros"] }
|
||||||
color-eyre = "0.6.5"
|
axum-extra = { version = "0", features = ["query"] }
|
||||||
futures = "0.3.31"
|
color-eyre = "0"
|
||||||
macaddr = { version = "1.0.1", features = ["serde", "serde_std"] }
|
futures = "0"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
macaddr = { version = "1", features = ["serde", "serde_std"] }
|
||||||
serde_json = "1.0.143"
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_with = { version = "3.14.0", features = ["json"] }
|
serde_html_form = "0"
|
||||||
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
|
serde_json = "1"
|
||||||
thiserror = "2.0.16"
|
serde_with = { version = "3", features = ["json"] }
|
||||||
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread"] }
|
strum = { version = "0", features = ["derive", "strum_macros"] }
|
||||||
urlencoding = "2.1.3"
|
thiserror = "2"
|
||||||
|
tokio = { version = "1", features = [
|
||||||
|
"fs",
|
||||||
|
"process",
|
||||||
|
"rt-multi-thread",
|
||||||
|
"io-util",
|
||||||
|
"macros",
|
||||||
|
] }
|
||||||
|
tower-http = { version = "0", features = ["fs"] }
|
||||||
|
urlencoding = "2"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
strip = true
|
strip = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["very-smart-parsing"]
|
||||||
|
very-smart-parsing = [
|
||||||
|
] # this is the a-bit-redundant parse thing that copilot made
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["ipjs"]
|
||||||
|
|
||||||
|
[dependencies.lda-ipjs]
|
||||||
|
path = "ipjs"
|
||||||
|
registry = "gitea"
|
||||||
|
version = "*"
|
||||||
@@ -2,19 +2,16 @@
|
|||||||
|
|
||||||
router shit
|
router shit
|
||||||
|
|
||||||
## usage
|
## whatever i did must be BEAUTIFUL
|
||||||
|
|
||||||
- Build (Windows host OK):
|
read [in vietnamese](https://ldlda.com/blogs/else/ssh-ax3000cv2-update-firmware-0-10-2)
|
||||||
- cargo build --release --target armv7-unknown-linux-musleabihf
|
|
||||||
- Run on a Linux box/router:
|
|
||||||
- copy target/armv7-unknown-linux-musleabihf/release/wakey to the device
|
|
||||||
- chmod +x wakey && ./wakey
|
|
||||||
- Install via release asset (OpenWrt):
|
|
||||||
- wget -O- `https://your.gitea/owner/repo/releases/download/vX.Y.Z/wakey-rootfs-vX.Y.Z-armv7-unknown-linux-musleabihf.tgz` | tar -xz -C /
|
|
||||||
- /etc/init.d/wakey enable && /etc/init.d/wakey start
|
|
||||||
|
|
||||||
Optional updater (fetch latest automatically on OpenWrt):
|
## now i NEED the split arch to be on
|
||||||
|
|
||||||
```sh
|
like RN rn rn so i can have web outside and the lil agent can be sitting here configuring itself (Parse/append to rc.local files? add to init.d? fuhh)
|
||||||
WAKEY_HOST=git.ldlda.com WAKEY_OWNER=lda WAKEY_REPO=wakey sh /etc/ldlda_help/update_wakey.sh
|
|
||||||
```
|
I NEED IT
|
||||||
|
|
||||||
|
I NEEED IT
|
||||||
|
|
||||||
|
I NEEEEEED it.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "lda-ipjs"
|
||||||
|
description = "ip -j show schemas"
|
||||||
|
version = "0.0.2"
|
||||||
|
edition = "2024"
|
||||||
|
publish = ["gitea"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
macaddr = { version = "1", features = ["serde", "serde_std"] }
|
||||||
|
strum = { version = "0", features = ["derive", "strum_macros"] }
|
||||||
|
serde_json = "1"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_with = { version = "3", features = ["json"] }
|
||||||
|
thiserror = "2"
|
||||||
|
anyhow = "1"
|
||||||
|
tokio = { version = "1", features = ["fs", "process", "rt-multi-thread", "io-util", "macros"] }
|
||||||
|
rtnetlink = "0"
|
||||||
|
futures = "0"
|
||||||
|
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_json::from_slice(&output.stdout).context("Deserialize failed")
|
||||||
|
}
|
||||||
|
pub async fn _get(dev: Option<&str>) -> io::Result<Output> {
|
||||||
|
let mut cmd = tokio::process::Command::new("ip");
|
||||||
|
cmd.args(["-j", "address", "show"]);
|
||||||
|
|
||||||
|
if let Some(d) = dev {
|
||||||
|
cmd.args(["dev", d]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.output().await
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//! ts
|
||||||
|
//!
|
||||||
|
//! deals with both ip a (addroutput) and ip l (commonoutput)
|
||||||
|
//!
|
||||||
|
//! lowk why its free but its indirection and its ass
|
||||||
|
|
||||||
|
pub mod json;
|
||||||
|
pub mod nl;
|
||||||
|
|
||||||
|
use crate::utils::serialize::mac::option_mac;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// i dont include what i dont know about (almost all ts)
|
||||||
|
#[derive(Serialize, Debug, Deserialize)]
|
||||||
|
pub struct AddrOutput {
|
||||||
|
pub ifindex: u32,
|
||||||
|
pub ifname: String,
|
||||||
|
/// i imagine UP or DOWN, unknown
|
||||||
|
pub operstate: String,
|
||||||
|
// 6 has a serde and the enum doesnt? why. (serializing ts is ass although... im not given an array. they string formatted ts)
|
||||||
|
#[serde(with = "option_mac", default)]
|
||||||
|
pub address: Option<MacAddr>,
|
||||||
|
#[serde(default)] // i wish we have intellisense for this... fuck you metaprogramming
|
||||||
|
pub addr_info: Vec<AddrInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// i be copying
|
||||||
|
// Raw JSON shape from ip -j -4 address show
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct AddrInfo {
|
||||||
|
pub family: Option<String>,
|
||||||
|
|
||||||
|
pub local: Option<String>,
|
||||||
|
pub prefixlen: Option<u8>,
|
||||||
|
|
||||||
|
pub broadcast: Option<String>,
|
||||||
|
|
||||||
|
pub scope: Option<String>,
|
||||||
|
pub label: Option<String>,
|
||||||
|
// many more exist; we only take what we need
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//! i said i aint doing ts no more why am i still here
|
||||||
|
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
|
||||||
|
use crate::subcommands::address::AddrOutput;
|
||||||
|
|
||||||
|
// shit this one is even worse you needa collect info from two places
|
||||||
|
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
|
||||||
|
let (conn, handle, _) = rtnetlink::new_connection()?;
|
||||||
|
tokio::spawn(conn); // every time?
|
||||||
|
let mut address = handle.address().get();
|
||||||
|
let mut link = handle.link().get();
|
||||||
|
if let Some(dev) = dev {
|
||||||
|
link = link.match_name(dev.to_owned());
|
||||||
|
if let Some(ind) = link.execute().try_next().await?.map(|a| a.header.index) {
|
||||||
|
address = address.set_link_index_filter(ind);
|
||||||
|
};
|
||||||
|
address.execute().try_next().await?;
|
||||||
|
} else {
|
||||||
|
};
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
//! this is for link. You need link; at least to build an index -> name map. i Need It. sometimes.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod address;
|
||||||
|
pub mod neighbor;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
//! idk what to put here
|
||||||
|
|
||||||
|
use std::{io, net::IpAddr, process::Output};
|
||||||
|
|
||||||
|
use anyhow::{Context, bail};
|
||||||
|
|
||||||
|
use super::{NUDState, NeighborItem};
|
||||||
|
|
||||||
|
// loose translation of [wakey::utils::query::macs::get_mac]
|
||||||
|
// i think ill write tokio::process every time tho (for this if let thing) because iterate through all ts youll have to as str and all the hooplas.
|
||||||
|
// it all turns to live osstr tho so ts just for my own sanity
|
||||||
|
// thiserror? anyhow
|
||||||
|
|
||||||
|
// what is vro sayin
|
||||||
|
// ahh. instead of using [wakey::utils::cmd::exec_command] which is ass we jus write everything out. so i dont have to .as_str() so often.
|
||||||
|
|
||||||
|
pub async fn get(
|
||||||
|
ip: Option<IpAddr>,
|
||||||
|
dev: Option<&str>,
|
||||||
|
nud: &[NUDState],
|
||||||
|
) -> anyhow::Result<Vec<NeighborItem>> {
|
||||||
|
let output = _get(ip, dev, nud).await.context("Can not run command")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
bail!(String::from_utf8_lossy(&output.stderr).into_owned())
|
||||||
|
} else {
|
||||||
|
let mut fuckass: Vec<NeighborItem> =
|
||||||
|
serde_json::from_slice(&output.stdout).context("Deserialize failed")?;
|
||||||
|
|
||||||
|
// i hate ts.
|
||||||
|
if let Some(dev) = dev {
|
||||||
|
for item in &mut fuckass {
|
||||||
|
if item.dev.is_none() {
|
||||||
|
item.dev = Some(dev.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(fuckass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn _get(ip: Option<IpAddr>, dev: Option<&str>, nud: &[NUDState]) -> io::Result<Output> {
|
||||||
|
let mut cmd = tokio::process::Command::new("ip");
|
||||||
|
cmd.args(["-j", "neigh", "show"]);
|
||||||
|
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
// cmd.arg("to");
|
||||||
|
cmd.arg(ip.to_canonical().to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(dev) = dev {
|
||||||
|
cmd.args(["dev", dev]);
|
||||||
|
}
|
||||||
|
for nud in nud {
|
||||||
|
cmd.arg("nud");
|
||||||
|
cmd.arg(nud.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.output().await
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//! ```bash
|
||||||
|
//! ip -j n s
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! yes. this is a real call.
|
||||||
|
|
||||||
|
pub mod json;
|
||||||
|
pub mod nl;
|
||||||
|
|
||||||
|
use crate::utils::serialize::mac::option_mac;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct NeighborInput {
|
||||||
|
/// supports only the last item (ignore), `to` keyword is optional
|
||||||
|
pub to: Option<IpAddr>,
|
||||||
|
/// supports only one item (it complains if multiple)
|
||||||
|
pub dev: Option<String>, // im all for simplicity
|
||||||
|
/// takes multiple, has to have `nud` before bro or it will think you `to`
|
||||||
|
pub nud: Vec<NUDState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// as input this must be lowercase. as output it is uppercase
|
||||||
|
/// docs for items come from a random ahh man website idk
|
||||||
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, Serialize, Deserialize, Default,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
pub enum NUDState {
|
||||||
|
/// the neighbour entry is valid forever and can
|
||||||
|
/// be only be removed administratively.
|
||||||
|
Permanent,
|
||||||
|
/// the neighbour entry is valid. No attempts to
|
||||||
|
/// validate this entry will be made but it can
|
||||||
|
/// be removed when its lifetime expires.
|
||||||
|
Noarp,
|
||||||
|
/// the neighbour entry is valid until the
|
||||||
|
/// reachability timeout expires.
|
||||||
|
Reachable,
|
||||||
|
/// the neighbour entry is valid but suspicious.
|
||||||
|
/// This option to ip neigh does not change the
|
||||||
|
/// neighbour state if it was valid and the
|
||||||
|
/// address is not changed by this command.
|
||||||
|
Stale,
|
||||||
|
/// this is a pseudo state used when initially
|
||||||
|
/// creating a neighbour entry or after trying to
|
||||||
|
/// remove it before it becomes free to do so.
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
/// the neighbour entry has not (yet) been
|
||||||
|
/// validated/resolved.
|
||||||
|
Incomplete,
|
||||||
|
/// neighbor entry validation is currently
|
||||||
|
/// delayed.
|
||||||
|
Delay,
|
||||||
|
/// neighbor is being probed.
|
||||||
|
Probe,
|
||||||
|
/// max number of probes exceeded without
|
||||||
|
/// success, neighbor validation has ultimately
|
||||||
|
/// failed.
|
||||||
|
Failed,
|
||||||
|
|
||||||
|
Other(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// everything i see
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct NeighborItem {
|
||||||
|
#[serde(rename(deserialize = "dst"))]
|
||||||
|
pub ip: IpAddr,
|
||||||
|
#[serde(default)]
|
||||||
|
pub dev: Option<String>,
|
||||||
|
#[serde(with = "option_mac", default, rename(deserialize = "lladdr"))]
|
||||||
|
pub mac: Option<MacAddr>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub state: Vec<NUDState>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
//! rtnetlink-based neighbor table query. One syscall, filter in userspace.
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
net::IpAddr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use rtnetlink::packet_route::{
|
||||||
|
AddressFamily,
|
||||||
|
link::LinkAttribute,
|
||||||
|
neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{NUDState, NeighborItem};
|
||||||
|
|
||||||
|
/// Fetch neighbors via rtnetlink. Empty slice = no filter (match all).
|
||||||
|
/// Non-empty slice = match ANY in the set.
|
||||||
|
// https://github.com/rust-netlink/rtnetlink/blob/main/examples/get_neighbours.rs
|
||||||
|
pub async fn get(
|
||||||
|
ips: &[IpAddr],
|
||||||
|
devs: &[impl AsRef<str>],
|
||||||
|
nuds: &[NUDState],
|
||||||
|
macs: &[MacAddr],
|
||||||
|
) -> anyhow::Result<Vec<NeighborItem>> {
|
||||||
|
let (conn, handle, _) = rtnetlink::new_connection()?;
|
||||||
|
tokio::spawn(conn);
|
||||||
|
|
||||||
|
let mut neighbor_data = handle.neighbours().get().execute();
|
||||||
|
|
||||||
|
// Build filter sets (empty = match all)
|
||||||
|
let ip_set: HashSet<IpAddr> = ips.iter().copied().collect();
|
||||||
|
let dev_set: HashSet<&str> = devs.iter().map(AsRef::as_ref).collect();
|
||||||
|
let nud_set: HashSet<&NUDState> = nuds.iter().collect();
|
||||||
|
let mac_set: HashSet<MacAddr> = macs.iter().copied().collect();
|
||||||
|
|
||||||
|
// Cache ifindex -> name
|
||||||
|
let mut ifname_cache: HashMap<u32, String> = HashMap::new();
|
||||||
|
let mut result = vec![];
|
||||||
|
|
||||||
|
'row: while let Some(msg) = neighbor_data.try_next().await? {
|
||||||
|
// Only IPv4/IPv6, skip NOARP
|
||||||
|
if !matches!(
|
||||||
|
msg.header.family,
|
||||||
|
AddressFamily::Inet | AddressFamily::Inet6
|
||||||
|
) || matches!(msg.header.state, NeighbourState::Noarp)
|
||||||
|
{
|
||||||
|
continue 'row;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state: NUDState = msg.header.state.try_into().unwrap_or_default();
|
||||||
|
let mut ip = None;
|
||||||
|
let mut mac = None;
|
||||||
|
|
||||||
|
for attr in msg.attributes {
|
||||||
|
match attr {
|
||||||
|
NeighbourAttribute::Destination(addr) => match addr {
|
||||||
|
NeighbourAddress::Inet(v4) => ip = Some(IpAddr::from(v4)),
|
||||||
|
NeighbourAddress::Inet6(v6) => ip = Some(IpAddr::from(v6)),
|
||||||
|
_ => continue 'row,
|
||||||
|
},
|
||||||
|
NeighbourAttribute::LinkLocalAddress(bytes) => {
|
||||||
|
mac = match bytes.len() {
|
||||||
|
6 => bytes.first_chunk::<6>().map(|&b| MacAddr::from(b)),
|
||||||
|
8 => bytes.first_chunk::<8>().map(|&b| MacAddr::from(b)),
|
||||||
|
_ => continue 'row,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve ifindex -> name (cached)
|
||||||
|
let dev = match ifname_cache.get(&msg.header.ifindex) {
|
||||||
|
Some(name) => Some(name.clone()),
|
||||||
|
None => {
|
||||||
|
let name = handle
|
||||||
|
.link()
|
||||||
|
.get()
|
||||||
|
.match_index(msg.header.ifindex)
|
||||||
|
.execute()
|
||||||
|
.try_next()
|
||||||
|
.await?
|
||||||
|
.and_then(|link| {
|
||||||
|
link.attributes.into_iter().find_map(|a| match a {
|
||||||
|
LinkAttribute::IfName(n) => Some(n),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if let Some(ref n) = name {
|
||||||
|
ifname_cache.insert(msg.header.ifindex, n.clone());
|
||||||
|
}
|
||||||
|
name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (Some(ip), Some(dev)) = (ip, dev) else {
|
||||||
|
continue 'row;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply filters (empty set = match all)
|
||||||
|
if !ip_set.is_empty() && !ip_set.contains(&ip) {
|
||||||
|
continue 'row;
|
||||||
|
}
|
||||||
|
if !dev_set.is_empty() && !dev_set.contains(dev.as_str()) {
|
||||||
|
continue 'row;
|
||||||
|
}
|
||||||
|
if !nud_set.is_empty() && !nud_set.contains(&state) {
|
||||||
|
continue 'row;
|
||||||
|
}
|
||||||
|
if !mac_set.is_empty() && !mac.is_some_and(|m| mac_set.contains(&m)) {
|
||||||
|
continue 'row;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(NeighborItem {
|
||||||
|
ip,
|
||||||
|
dev: Some(dev),
|
||||||
|
mac,
|
||||||
|
state: vec![state],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<NeighbourState> for NUDState {
|
||||||
|
fn try_from(value: NeighbourState) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
NeighbourState::Incomplete => Ok(Self::Incomplete),
|
||||||
|
NeighbourState::Reachable => Ok(Self::Reachable),
|
||||||
|
NeighbourState::Stale => Ok(Self::Stale),
|
||||||
|
NeighbourState::Delay => Ok(Self::Delay),
|
||||||
|
NeighbourState::Probe => Ok(Self::Probe),
|
||||||
|
NeighbourState::Failed => Ok(Self::Failed),
|
||||||
|
NeighbourState::Noarp => Ok(Self::Noarp),
|
||||||
|
NeighbourState::Permanent => Ok(Self::Permanent),
|
||||||
|
NeighbourState::None => Ok(Self::None),
|
||||||
|
NeighbourState::Other(e) => Ok(Self::Other(e)),
|
||||||
|
_ => Err(u16::MAX),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error = u16;
|
||||||
|
}
|
||||||
@@ -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,50 @@
|
|||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
|
||||||
|
pub fn serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let strings: Vec<String> = macs.iter().map(|m| m.to_string()).collect();
|
||||||
|
serde::Serialize::serialize(&strings, serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize a MacAddr as a string
|
||||||
|
pub fn serialize<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&mac.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize a MacAddr from a string
|
||||||
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||||
|
s.parse::<MacAddr>().map_err(DeError::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod option_mac {
|
||||||
|
use super::*;
|
||||||
|
/// serialize an [`Option<MacAddr>`]
|
||||||
|
pub fn serialize<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
|
||||||
|
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
|
||||||
|
}
|
||||||
|
/// deserialize an [`Option<MacAddr>`], returns None for invalid input (::, 0.0.0.0)
|
||||||
|
pub fn deserialize<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: Option<&str> = Option::<&str>::deserialize(des)?;
|
||||||
|
match s {
|
||||||
|
Some(val) => match val.parse::<MacAddr>() {
|
||||||
|
Ok(mac) => Ok(Some(mac)),
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
},
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,139 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use lda_ipjs::subcommands::{address, neighbor};
|
||||||
|
|
||||||
|
#[tokio::test] // ← Use tokio::test instead of manual #[tokio::main]
|
||||||
|
async fn ball1() -> anyhow::Result<()> {
|
||||||
|
let result = neighbor::nl::get(&[], &[] as &[&str], &[], &[]).await?;
|
||||||
|
println!("netlink results: {:?}", result);
|
||||||
|
Ok(()) // ← Don't force error, let it succeed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ball2() -> anyhow::Result<()> {
|
||||||
|
let result = neighbor::json::get(None, None, &[]).await?;
|
||||||
|
println!("json results: {:?}", result);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this to debug the raw JSON
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ball_raw_json() -> anyhow::Result<()> {
|
||||||
|
let output = tokio::process::Command::new("ip")
|
||||||
|
.args(["-j", "neigh", "show"])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let json = String::from_utf8_lossy(&output.stdout);
|
||||||
|
println!("Raw JSON:\n{}", json);
|
||||||
|
|
||||||
|
// Try to parse it
|
||||||
|
let parsed: Result<Vec<neighbor::NeighborItem>, _> = serde_json::from_slice(&output.stdout);
|
||||||
|
match parsed {
|
||||||
|
Ok(items) => println!("Parsed {} items", items.len()),
|
||||||
|
Err(e) => println!("Parse error: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check what fields actually exist in the JSON
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ball_field_analysis() -> anyhow::Result<()> {
|
||||||
|
let output = tokio::process::Command::new("ip")
|
||||||
|
.args(["-j", "neigh", "show"])
|
||||||
|
.output()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let raw: Vec<serde_json::Value> = serde_json::from_slice(&output.stdout)?;
|
||||||
|
|
||||||
|
println!("Found {} neighbor entries", raw.len());
|
||||||
|
|
||||||
|
// Collect all unique field names across all entries
|
||||||
|
let mut all_fields = std::collections::HashSet::new();
|
||||||
|
for (i, entry) in raw.iter().enumerate() {
|
||||||
|
if let Some(obj) = entry.as_object() {
|
||||||
|
println!("\nEntry {}: {} fields", i, obj.len());
|
||||||
|
for (key, value) in obj {
|
||||||
|
all_fields.insert(key.clone());
|
||||||
|
println!(" {}: {} = {:?}", key, value.type_name(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n=== All unique fields seen ===");
|
||||||
|
for field in &all_fields {
|
||||||
|
println!(" - {}", field);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper trait to get type name for JSON values
|
||||||
|
trait TypeName {
|
||||||
|
fn type_name(&self) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeName for serde_json::Value {
|
||||||
|
fn type_name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
serde_json::Value::Null => "null",
|
||||||
|
serde_json::Value::Bool(_) => "bool",
|
||||||
|
serde_json::Value::Number(_) => "number",
|
||||||
|
serde_json::Value::String(_) => "string",
|
||||||
|
serde_json::Value::Array(_) => "array",
|
||||||
|
serde_json::Value::Object(_) => "object",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test filtering logic
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ball_compare_backends() -> anyhow::Result<()> {
|
||||||
|
println!("=== JSON Backend ===");
|
||||||
|
let json_result = neighbor::json::get(None, None, &[]).await?;
|
||||||
|
println!("Got {} entries from JSON", json_result.len());
|
||||||
|
|
||||||
|
println!("\n=== Netlink Backend ===");
|
||||||
|
let nl_result = neighbor::nl::get(&[], &[] as &[&str], &[], &[]).await?;
|
||||||
|
println!("Got {} entries from netlink", nl_result.len());
|
||||||
|
|
||||||
|
// Compare counts
|
||||||
|
if json_result.len() != nl_result.len() {
|
||||||
|
println!(
|
||||||
|
"\n⚠️ Count mismatch! JSON: {}, Netlink: {}",
|
||||||
|
json_result.len(),
|
||||||
|
nl_result.len()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("\n✅ Both backends returned same count");
|
||||||
|
}
|
||||||
|
let a: HashSet<neighbor::NeighborItem> = HashSet::from_iter(json_result);
|
||||||
|
let b: HashSet<neighbor::NeighborItem> = HashSet::from_iter(nl_result);
|
||||||
|
println!(
|
||||||
|
"istg {len1} == {len2} or else",
|
||||||
|
len1 = a.len(),
|
||||||
|
len2 = b.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[tokio::test]
|
||||||
|
// async fn cidr_filter() {
|
||||||
|
// unimplemented!("never. i aint add what i dont need")
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ipjas() -> anyhow::Result<()> {
|
||||||
|
let cuh = address::json::get(None).await?;
|
||||||
|
println!("{cuh:#?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ipjas_raw() -> anyhow::Result<()> {
|
||||||
|
let cuh = address::json::_get(None).await?;
|
||||||
|
println!("{cuh:#?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# this does one thing
|
||||||
|
param(
|
||||||
|
[string]$PASSWD
|
||||||
|
)
|
||||||
|
|
||||||
|
# idk what this does it works like that then thats how it is
|
||||||
|
pscp.exe -l root -scp -pw $PASSWD -r 192.168.100.1:/etc/ldlda_help $PSScriptRoot
|
||||||
|
|
||||||
|
$initDir = Join-Path $PSScriptRoot 'init.d'
|
||||||
|
New-Item -ItemType Directory -Force -Path $initDir | Out-Null
|
||||||
|
|
||||||
|
# these are sum
|
||||||
|
$files = "update_wakey" , "wakey" , "update_tailscale" , "wireguard_setup"
|
||||||
|
$remote = $files.ForEach({ "/etc/init.d/$_" })
|
||||||
|
$remote | ForEach-Object {
|
||||||
|
pscp.exe -l root -scp -pw $PASSWD "192.168.100.1:$_" "$initDir\"
|
||||||
|
}
|
||||||
+32
-24
@@ -4,42 +4,44 @@ This folder has small helpers for build, CI, and router install. Keep it simple;
|
|||||||
|
|
||||||
## Quick start (recommended)
|
## Quick start (recommended)
|
||||||
|
|
||||||
1. Start your Gitea runner on this Windows PC
|
1. Start your Gitea runner on this Windows PC (if not already running):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
./scripts/act_runner.ps1
|
./scripts/act_runner.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
If first-time, it prints the exact register command. Run it once, check labels include `windows:host,self-hosted`, then rerun the script.
|
If first-time, it prints the exact register command. Run it once, check labels include `windows:host,self-hosted`, then rerun the script.
|
||||||
|
|
||||||
1. Tag and push to trigger CI
|
2. Publish, tag, and push (recommended):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git tag v0.1.0
|
./scripts/publish.ps1 -Tag
|
||||||
git push origin v0.1.0
|
```
|
||||||
```
|
|
||||||
|
|
||||||
1. Install on the router
|
This will bump the version (if you specify one), build, ensure the runner is started, tag, and push. To also publish to the registry, add `-Publish`.
|
||||||
|
|
||||||
Use the release asset URL from your Gitea Release page:
|
3. Install on the router
|
||||||
|
|
||||||
```sh
|
Use the updater on the router to fetch the latest automatically:
|
||||||
wget -O- https://<gitea>/<owner>/<repo>/releases/download/v0.1.0/wakey-rootfs-v0.1.0-armv7-unknown-linux-musleabihf.tgz | tar -xz -C /
|
|
||||||
chmod +x /etc/init.d/wakey && /etc/init.d/wakey enable && /etc/init.d/wakey restart
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, use the updater on the router to fetch the latest automatically:
|
```sh
|
||||||
|
WAKEY_HOST=git.ldlda.com WAKEY_OWNER=lda WAKEY_REPO=wakey sh /etc/ldlda_help/update_wakey.sh
|
||||||
|
```
|
||||||
|
|
||||||
```sh
|
Alternatively, use the release asset URL from your Gitea Release page:
|
||||||
WAKEY_HOST=git.ldlda.com WAKEY_OWNER=lda WAKEY_REPO=wakey sh /etc/ldlda_help/update_wakey.sh
|
|
||||||
```
|
```sh
|
||||||
|
wget -O- https://<gitea>/<owner>/<repo>/releases/download/v0.1.0/wakey-rootfs-v0.1.0-armv7-unknown-linux-musleabihf.tgz | tar -xz -C /
|
||||||
|
chmod +x /etc/init.d/wakey && /etc/init.d/wakey enable && /etc/init.d/wakey restart
|
||||||
|
```
|
||||||
|
|
||||||
## Scripts overview
|
## Scripts overview
|
||||||
|
|
||||||
- `act_runner.ps1` — Start/seed the local Gitea runner. Use `-Attach` to see logs, `-ForceConfigure` to register non-interactively.
|
- `act_runner.ps1` — Start/seed the local Gitea runner. Use `-Attach` to see logs, `-ForceConfigure` to register non-interactively.
|
||||||
- `cross_build.ps1` — Simple wrapper for `cargo build` per target (default: armv7-unknown-linux-musleabihf).
|
- `dev_push.ps1` — Fast dev loop: build + upload to router. Usage:
|
||||||
|
- `./scripts/dev_push.ps1 -Pass <password> [-HostName <ip>] [-RemotePath </root/.bin/wakey>] [-Restart] [-Quiet]`
|
||||||
|
- Uploads to `<RemotePath>.tmp` then atomically moves into place; `-Restart` restarts the service; `-Quiet` silences MOTD.
|
||||||
- `package_rootfs.ps1` — Produces `dist/wakey-rootfs-<version>-<target>.tgz` with `/root/.bin/wakey` and `/etc/init.d/*`.
|
- `package_rootfs.ps1` — Produces `dist/wakey-rootfs-<version>-<target>.tgz` with `/root/.bin/wakey` and `/etc/init.d/*`.
|
||||||
- `package.ps1` — Dev bundle with binaries + init scripts (not a rootfs layout).
|
|
||||||
- `publish.ps1` — Optional version bump + build + tag (and `cargo publish` only if you pass `-Publish`).
|
- `publish.ps1` — Optional version bump + build + tag (and `cargo publish` only if you pass `-Publish`).
|
||||||
|
|
||||||
## CI (Gitea)
|
## CI (Gitea)
|
||||||
@@ -52,8 +54,8 @@ WAKEY_HOST=git.ldlda.com WAKEY_OWNER=lda WAKEY_REPO=wakey sh /etc/ldlda_help/upd
|
|||||||
## Local build/install (manual path)
|
## Local build/install (manual path)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Build (requires cross installed)
|
# Build
|
||||||
./scripts/cross_build.ps1 -Release -Targets armv7-unknown-linux-musleabihf
|
cargo build --release --target armv7-unknown-linux-musleabihf
|
||||||
|
|
||||||
# Package rootfs
|
# Package rootfs
|
||||||
./scripts/package_rootfs.ps1 -Version v0.1.0 -Target armv7-unknown-linux-musleabihf
|
./scripts/package_rootfs.ps1 -Version v0.1.0 -Target armv7-unknown-linux-musleabihf
|
||||||
@@ -68,4 +70,10 @@ tar -xz -f /tmp/wakey.tgz -C /
|
|||||||
chmod +x /etc/init.d/wakey && /etc/init.d/wakey enable && /etc/init.d/wakey restart
|
chmod +x /etc/init.d/wakey && /etc/init.d/wakey enable && /etc/init.d/wakey restart
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or you can use the script:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./scripts/dev_push.ps1 -Pass <password>
|
||||||
|
```
|
||||||
|
|
||||||
That’s it. Keep the flow: start runner → tag push → install.
|
That’s it. Keep the flow: start runner → tag push → install.
|
||||||
+19
-6
@@ -10,7 +10,9 @@ param(
|
|||||||
# Opt-in to non-interactive configure; by default we print the command for you to run manually.
|
# Opt-in to non-interactive configure; by default we print the command for you to run manually.
|
||||||
[switch]$ForceConfigure,
|
[switch]$ForceConfigure,
|
||||||
# When -Attach, run in the foreground to see logs (good for first-time troubleshooting)
|
# When -Attach, run in the foreground to see logs (good for first-time troubleshooting)
|
||||||
[switch]$Attach
|
[switch]$Attach,
|
||||||
|
# Run one job then exit (great for CI tag pushes)
|
||||||
|
[switch]$Once
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -38,7 +40,10 @@ if (-not (Test-Path $configFile)) {
|
|||||||
}
|
}
|
||||||
if ($ForceConfigure) {
|
if ($ForceConfigure) {
|
||||||
Write-Host "Config not found. Initializing non-interactively (ForceConfigure)..."
|
Write-Host "Config not found. Initializing non-interactively (ForceConfigure)..."
|
||||||
|
$configParent = Split-Path -Parent $configFile
|
||||||
|
Push-Location $configParent
|
||||||
& $RunnerPath register --no-interactive --config $configFile --instance $ServerUrl --token $Token --labels $Labels
|
& $RunnerPath register --no-interactive --config $configFile --instance $ServerUrl --token $Token --labels $Labels
|
||||||
|
Pop-Location
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Host "Config not found. Run this manually once (note labels embed host executor on Windows):"
|
Write-Host "Config not found. Run this manually once (note labels embed host executor on Windows):"
|
||||||
@@ -47,11 +52,19 @@ if (-not (Test-Path $configFile)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($Attach) {
|
if ($Attach -or $Once) {
|
||||||
Write-Host "Starting act_runner (attached)... Ctrl+C to stop"
|
Write-Host "Starting act_runner $(if ($Once) {'(once mode)'} else {'(attached)'})... $(if (-not $Once) {'Ctrl+C to stop'})"
|
||||||
& $RunnerPath daemon --config $configFile
|
$configParent = Split-Path -Parent $configFile
|
||||||
|
Push-Location $configParent
|
||||||
|
$daemonArgs = @('daemon', '--config', $configFile)
|
||||||
|
if ($Once) { $daemonArgs += '--once' }
|
||||||
|
& $RunnerPath @daemonArgs
|
||||||
|
Pop-Location
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Start-Process -FilePath $RunnerPath -ArgumentList @("daemon", "--config", $configFile) -WindowStyle Hidden
|
$configParent = Split-Path -Parent $configFile
|
||||||
Write-Host "act_runner started."
|
Push-Location $configParent
|
||||||
|
$proc = Start-Process -FilePath $RunnerPath -ArgumentList @("daemon", "--config", $configFile) -WindowStyle Hidden -PassThru
|
||||||
|
Pop-Location
|
||||||
|
Write-Host "act_runner started (PID=$($proc.Id))."
|
||||||
}
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Build release binaries using cross for Linux targets.
|
|
||||||
# Requires: cross installed (cargo install cross)
|
|
||||||
param(
|
|
||||||
[string[]]$Targets = @("armv7-unknown-linux-musleabihf"),
|
|
||||||
[switch]$Release
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$mode = if ($Release) { "--release" } else { "" }
|
|
||||||
|
|
||||||
foreach ($t in $Targets) {
|
|
||||||
Write-Host "Building for $t..."
|
|
||||||
cross build $mode --target $t
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Done."
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', "",
|
||||||
|
Justification = 'Default true is intentional for fast dev loop')]
|
||||||
|
param(
|
||||||
|
[string]$Pass,
|
||||||
|
[string]$HostName = "192.168.100.1",
|
||||||
|
[string]$User = "root",
|
||||||
|
[string]$RemotePath = "/root/.bin/wakey",
|
||||||
|
[string]$Target = "armv7-unknown-linux-musleabihf",
|
||||||
|
[string]$BinName = "wakey",
|
||||||
|
[string]$HostKey,
|
||||||
|
[switch]$Restart = $true,
|
||||||
|
[switch]$Quiet = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
try { $PSStyle.OutputRendering = 'Host' } catch {}
|
||||||
|
|
||||||
|
. "$PSScriptRoot/lib.ps1"
|
||||||
|
|
||||||
|
function Get-DeployScript {
|
||||||
|
param($DeployPreferred, $DeployTmp, $RemoteTmp, $RemotePath, $RestartFlag)
|
||||||
|
return @"
|
||||||
|
DEPLOY=$DeployPreferred;
|
||||||
|
if [ ! -x "`$DEPLOY" ] && [ -f $DeployTmp ]; then
|
||||||
|
sed -i "s/\r$//" $DeployTmp 2>/dev/null || true
|
||||||
|
chmod +x $DeployTmp
|
||||||
|
DEPLOY=$DeployTmp
|
||||||
|
fi
|
||||||
|
sh "`$DEPLOY" $RemoteTmp $RemotePath $RestartFlag
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
#---- Main Flow ----
|
||||||
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
Push-Location $repoRoot
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Host "[build] cargo build --release --target $Target" -ForegroundColor Cyan
|
||||||
|
cargo build --release --target $Target
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "cargo build failed ($LASTEXITCODE)" }
|
||||||
|
|
||||||
|
$localBin = Join-Path $repoRoot "target/$Target/release/$BinName"
|
||||||
|
if (-not (Test-Path $localBin)) { throw "binary not found: $localBin" }
|
||||||
|
|
||||||
|
$remoteTmp = "$RemotePath.tmp"
|
||||||
|
$destTmp = "$User@${HostName}:$remoteTmp"
|
||||||
|
$localDeploy = Join-Path $repoRoot 'scripts/remote_deploy_wakey.sh'
|
||||||
|
$deployTmp = '/var/tmp/remote_deploy_wakey.sh'
|
||||||
|
$deployPreferred = '/root/.bin/remote_deploy_wakey.sh'
|
||||||
|
|
||||||
|
# Push main binary
|
||||||
|
Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Quiet:$Quiet
|
||||||
|
|
||||||
|
# Push static assets
|
||||||
|
$localStatic = Join-Path $repoRoot "static"
|
||||||
|
if (Test-Path $localStatic) {
|
||||||
|
# Assuming RemotePath is like /root/.bin/wakey, we want /root/.bin/static
|
||||||
|
# So we push 'static' directory to /root/.bin/
|
||||||
|
$remoteDir = (Split-Path $RemotePath -Parent) -replace '\\', '/'
|
||||||
|
# Ensure remote dir exists (ssh mkdir -p)
|
||||||
|
Invoke-Ssh -Cmd "mkdir -p $remoteDir" -User $User -Remote $HostName -Pass $Pass -Quiet:$Quiet
|
||||||
|
|
||||||
|
# SCP -r static user@host:/root/.bin/
|
||||||
|
# Note: pscp/scp behavior: if dest is a dir, it copies the source dir INTO it.
|
||||||
|
Invoke-Scp -Local $localStatic -Dest "$User@${HostName}:$remoteDir/" -Pass $Pass -HostKey $HostKey -Quiet:$Quiet -Recurse
|
||||||
|
}
|
||||||
|
|
||||||
|
# Push deploy helper if exists
|
||||||
|
if (Test-Path $localDeploy) {
|
||||||
|
Invoke-Scp -Local $localDeploy -Dest "$User@${HostName}:$deployTmp" -Pass $Pass -HostKey $HostKey -Quiet:$Quiet
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build and run remote deploy command
|
||||||
|
$restartFlag = $(if ($Restart) { '1' } else { '0' })
|
||||||
|
$script = Get-DeployScript $deployPreferred $deployTmp $remoteTmp $RemotePath $restartFlag
|
||||||
|
$remoteCmd = "sh -lc '$($script -replace "`r",'')'"
|
||||||
|
if ($Quiet) { $remoteCmd += " >/dev/null 2>&1" }
|
||||||
|
|
||||||
|
Invoke-Ssh -Cmd $remoteCmd -User $User -Remote $HostName -Pass $Pass -Quiet:$Quiet
|
||||||
|
|
||||||
|
Write-Host "done ✔" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
@@ -7,18 +7,24 @@
|
|||||||
# Default-Stop:
|
# Default-Stop:
|
||||||
# Short-Description: Pre-flight Tailscale update
|
# Short-Description: Pre-flight Tailscale update
|
||||||
|
|
||||||
START=50 # Run before Tailscale (typically START~80)
|
START=50 # Run before Tailscale (typically START~80)
|
||||||
USE_PROCD=1
|
|
||||||
|
|
||||||
start_service() {
|
do_update() {
|
||||||
procd_open_instance
|
MAX_RETRIES=10
|
||||||
procd_set_param command /etc/ldlda_help/update_tailscale.sh
|
RETRY_COUNT=0
|
||||||
procd_set_param respawn 0 0 0
|
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
|
||||||
procd_set_param stdout 1
|
if /etc/ldlda_help/update_tailscale.sh; then
|
||||||
procd_set_param stderr 1
|
echo "[update_tailscale] Success at $(date)"
|
||||||
procd_close_instance
|
return 0
|
||||||
|
fi
|
||||||
|
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||||
|
echo "[update_tailscale] Retry $RETRY_COUNT at $(date)"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "[update_tailscale] Too many retries. Exiting at $(date)"
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_service() {
|
start() {
|
||||||
: # one-shot
|
do_update >> /var/log/update_tailscale.log 2>&1 &
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
#!/bin/sh /etc/rc.common
|
#!/bin/sh /etc/rc.common
|
||||||
|
START=66
|
||||||
|
|
||||||
START=66 # Run after the resolvconf swap
|
# does this work now fam
|
||||||
USE_PROCD=1
|
# exposal to wg is separate; so weon care
|
||||||
|
do_update() {
|
||||||
start_service() {
|
MAX_RETRIES=10
|
||||||
procd_open_instance
|
RETRY_COUNT=0
|
||||||
procd_set_param command /etc/ldlda_help/update_wakey.sh
|
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
|
||||||
procd_set_param respawn 0 0 0
|
if /etc/ldlda_help/update_wakey.sh; then
|
||||||
procd_set_param stdout 1
|
echo "[update_wakey] Success at $(date)"
|
||||||
procd_set_param stderr 1
|
return 0
|
||||||
procd_close_instance
|
fi
|
||||||
|
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||||
|
echo "[update_wakey] Retry $RETRY_COUNT at $(date)"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
echo "[update_wakey] Too many retries. Exiting at $(date)"
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_service() {
|
start() {
|
||||||
: # one-shot
|
do_update >>/var/log/update_wakey.log 2>&1 &
|
||||||
}
|
}
|
||||||
@@ -8,14 +8,14 @@ NAME=wakey
|
|||||||
BIN=/root/.bin/wakey
|
BIN=/root/.bin/wakey
|
||||||
|
|
||||||
start_service() {
|
start_service() {
|
||||||
procd_open_instance
|
procd_open_instance
|
||||||
procd_set_param command "$BIN"
|
procd_set_param command "$BIN"
|
||||||
procd_set_param respawn 5 1 0
|
procd_set_param respawn 5 1 0
|
||||||
procd_set_param stdout 1
|
procd_set_param stdout 1
|
||||||
procd_set_param stderr 1
|
procd_set_param stderr 1
|
||||||
procd_close_instance
|
procd_close_instance
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_service() {
|
stop_service() {
|
||||||
: # procd manages the process; nothing to do here
|
: # procd manages the process; nothing to do here
|
||||||
}
|
}
|
||||||
@@ -6,20 +6,20 @@ pids="$(pidof wakey 2>/dev/null)"
|
|||||||
|
|
||||||
# 2) Fallback: ps | awk (avoid matching awk/grep themselves)
|
# 2) Fallback: ps | awk (avoid matching awk/grep themselves)
|
||||||
if [ -z "$pids" ]; then
|
if [ -z "$pids" ]; then
|
||||||
pids="$(ps w 2>/dev/null | awk '/\/\.bin\/wakey/ && $0 !~ /awk/ {print $1}')"
|
pids="$(ps w 2>/dev/null | awk '/\/\.bin\/wakey/ && $0 !~ /awk/ {print $1}')"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -n "$pids" ] || exit 0
|
[ -n "$pids" ] || exit 0
|
||||||
|
|
||||||
# Send TERM first
|
# Send TERM first
|
||||||
kill -TERM $pids 2>/dev/null || true
|
kill -TERM "$pids" 2>/dev/null || true
|
||||||
|
|
||||||
# Optional: hard kill if still alive after a short grace
|
# Optional: hard kill if still alive after a short grace
|
||||||
sleep 0.2
|
usleep 250000 # uhhh sleep is stupid
|
||||||
remain=""
|
remain=""
|
||||||
for p in $pids; do
|
for p in $pids; do
|
||||||
kill -0 "$p" 2>/dev/null && remain="$remain $p"
|
kill -0 "$p" 2>/dev/null && remain="$remain $p"
|
||||||
done
|
done
|
||||||
[ -z "$remain" ] || kill -KILL $remain 2>/dev/null || true
|
[ -z "$remain" ] || kill -KILL "$remain" 2>/dev/null || true
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
@@ -2,24 +2,33 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
# 0th step from lda is to move the pre assed resolv.conf to the real resolv.conf
|
# 0th step from lda is to move the pre assed resolv.conf to the real resolv.conf
|
||||||
[ -f /tmp/resolv.conf ] && cp -af /tmp/resolv.conf /etc/resolv.conf || ([ -f /rom/etc/resolv.conf ] && cp -af /rom/etc/resolv.conf /etc/resolv.conf)
|
if [ -f /tmp/resolv.conf ]; then
|
||||||
|
cp -af /tmp/resolv.conf /etc/resolv.conf
|
||||||
|
elif [ -f /rom/etc/resolv.conf ]; then
|
||||||
|
cp -af /rom/etc/resolv.conf /etc/resolv.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x "$(which tailscale)" ]; then
|
||||||
|
tailscale update
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# 1. Fetch the latest version number from Tailscale's site
|
# 1. Fetch the latest version number from Tailscale's site
|
||||||
LATEST=$(curl -s https://pkgs.tailscale.com/stable/ | grep -Eo 'tailscale_[0-9\.]+_arm.tgz' | head -n1)
|
LATEST=$(curl -s https://pkgs.tailscale.com/stable/ | grep -Eo 'tailscale_[0-9\.]+_arm.tgz' | head -n1)
|
||||||
|
|
||||||
[ -z "$LATEST" ] && {
|
[ -z "$LATEST" ] && {
|
||||||
echo "❌ Couldn't find latest ARM binary."
|
echo "❌ Couldn't find latest ARM binary."
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
URL="https://pkgs.tailscale.com/stable/${LATEST}"
|
URL="https://pkgs.tailscale.com/stable/${LATEST}"
|
||||||
|
|
||||||
cd /var/tmp
|
cd /var/tmp
|
||||||
|
|
||||||
[ -f "$LATEST" ] && {
|
if [ -f "$LATEST" ]; then
|
||||||
echo "file exists"
|
echo "file exists"
|
||||||
rm "$LATEST"
|
rm "$LATEST"
|
||||||
}
|
fi
|
||||||
|
|
||||||
echo "Downloading $LATEST..."
|
echo "Downloading $LATEST..."
|
||||||
wget -q "$URL"
|
wget -q "$URL"
|
||||||
|
|||||||
@@ -15,91 +15,122 @@
|
|||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
# reboot == new file.
|
||||||
|
FLAG=/var/wakey_updated.flag
|
||||||
ARCH=${WAKEY_ARCH:-armv7-unknown-linux-musleabihf}
|
ARCH=${WAKEY_ARCH:-armv7-unknown-linux-musleabihf}
|
||||||
TMPFILE="/tmp/wakey-rootfs.$$.$ARCH.tgz"
|
TMPDIR="/var/tmp"
|
||||||
|
TMPFILE="$TMPDIR/wakey-rootfs.$$.$ARCH.tgz"
|
||||||
|
STAGING="$TMPDIR/wakey-rootfs.$$.$ARCH"
|
||||||
|
|
||||||
log() { echo "[update_wakey] $*"; }
|
log() { echo "[update_wakey] $*"; }
|
||||||
fail() { echo "[update_wakey] ERROR: $*" >&2; exit 1; }
|
fail() {
|
||||||
|
echo "[update_wakey] ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
http_get() {
|
http_get() {
|
||||||
# http_get <url>
|
# http_get <url>
|
||||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||||
# uclient-fetch supports -O -, write to stdout
|
# uclient-fetch supports -O -, write to stdout
|
||||||
uclient-fetch -O - "$1" 2>/dev/null || return 1
|
uclient-fetch -O - "$1" 2>/dev/null || return 1
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
|
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
|
||||||
elif command -v curl >/dev/null 2>&1; then
|
elif command -v curl >/dev/null 2>&1; then
|
||||||
if [ -n "${WAKEY_INSECURE:-}" ]; then
|
if [ -n "${WAKEY_INSECURE:-}" ]; then
|
||||||
curl -fsSL -k "$1" || return 1
|
curl -fsSL -k "$1" || return 1
|
||||||
else
|
else
|
||||||
curl -fsSL "$1" || return 1
|
curl -fsSL "$1" || return 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch() {
|
fetch() {
|
||||||
# fetch <url> <out>
|
# fetch <url> <out>
|
||||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||||
uclient-fetch -O "$2" "$1" || return 1
|
uclient-fetch -O "$2" "$1" || return 1
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
# shellcheck disable=SC2086
|
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
|
||||||
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
|
elif command -v curl >/dev/null 2>&1; then
|
||||||
elif command -v curl >/dev/null 2>&1; then
|
if [ -n "${WAKEY_INSECURE:-}" ]; then
|
||||||
if [ -n "${WAKEY_INSECURE:-}" ]; then
|
curl -fSL -k -o "$2" "$1" || return 1
|
||||||
curl -fSL -k -o "$2" "$1" || return 1
|
else
|
||||||
else
|
curl -fSL -o "$2" "$1" || return 1
|
||||||
curl -fSL -o "$2" "$1" || return 1
|
fi
|
||||||
fi
|
else
|
||||||
else
|
return 2
|
||||||
return 2
|
fi
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
latest_asset_url() {
|
latest_asset_url() {
|
||||||
# prints URL to stdout, returns 0 on success (public release only)
|
# prints URL to stdout, returns 0 on success (public release only)
|
||||||
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
|
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
|
||||||
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
|
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
|
||||||
RESP=$(http_get "$API") || return 1
|
RESP=$(http_get "$API") || return 1
|
||||||
# Extract the first browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
|
# Extract the LAST browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
|
||||||
echo "$RESP" \
|
echo "$RESP" |
|
||||||
| grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' \
|
grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' |
|
||||||
| head -n1 \
|
tail -n1 |
|
||||||
| cut -d '"' -f 4
|
cut -d '"' -f 4
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
URL="${WAKEY_TGZ_URL:-}"
|
[ -f "$FLAG" ] && return
|
||||||
if [ -z "$URL" ]; then
|
URL="${WAKEY_TGZ_URL:-}"
|
||||||
HOST=${WAKEY_HOST:-git.ldlda.com}
|
if [ -z "$URL" ]; then
|
||||||
OWNER=${WAKEY_OWNER:-lda}
|
HOST=${WAKEY_HOST:-git.ldlda.com}
|
||||||
REPO=${WAKEY_REPO:-wakey}
|
OWNER=${WAKEY_OWNER:-lda}
|
||||||
if [ -n "${WAKEY_VERSION:-}" ]; then
|
REPO=${WAKEY_REPO:-wakey}
|
||||||
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
|
if [ -n "${WAKEY_VERSION:-}" ]; then
|
||||||
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
|
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
|
||||||
else
|
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
|
||||||
URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL"
|
else
|
||||||
[ -n "$URL" ] || fail "no asset URL found for arch $ARCH"
|
URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL"
|
||||||
fi
|
[ -n "$URL" ] || fail "no asset URL found for arch $ARCH"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
log "fetching $URL"
|
log "fetching $URL"
|
||||||
if ! fetch "$URL" "$TMPFILE"; then
|
if ! fetch "$URL" "$TMPFILE"; then
|
||||||
fail "download failed"
|
fail "download failed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "installing"
|
log "installing"
|
||||||
tar -xz -f "$TMPFILE" -C / || fail "extract failed"
|
mkdir -p "$TMPDIR" "$STAGING"
|
||||||
rm -f "$TMPFILE"
|
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
|
||||||
|
|
||||||
if [ -f /etc/init.d/wakey ]; then
|
# Ensure execute bits on staged files we know should be executable
|
||||||
chmod +x /etc/init.d/wakey || true
|
for f in \
|
||||||
/etc/init.d/wakey enable || true
|
"$STAGING/etc/init.d/"* \
|
||||||
/etc/init.d/wakey restart || /etc/init.d/wakey start || true
|
"$STAGING/etc/ldlda_help/"*.sh \
|
||||||
fi
|
"$STAGING/root/.bin/wakey" \
|
||||||
log "done"
|
"$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
|
||||||
|
|
||||||
|
# Copy staged tree into /
|
||||||
|
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
|
||||||
|
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Shared functions for wakey scripts
|
||||||
|
|
||||||
|
function Invoke-Ext {
|
||||||
|
param($Exe, $Arguments, $Label)
|
||||||
|
$displayArgs = $Arguments.Clone()
|
||||||
|
for ($i = 0; $i -lt $displayArgs.Count; $i++) {
|
||||||
|
if ($displayArgs[$i] -eq '-pw' -and ($i + 1) -lt $displayArgs.Count) {
|
||||||
|
$displayArgs[$i + 1] = '****'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host ("[{0}] {1} {2}" -f $Label, $Exe, ($displayArgs -join ' ')) -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$out = & $Exe @Arguments 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error ("{0} failed ({1}):`n{2}" -f $Label, $LASTEXITCODE, ($out -join "`n"))
|
||||||
|
throw ("{0} failed ({1})" -f $Label, $LASTEXITCODE)
|
||||||
|
}
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Scp {
|
||||||
|
param($Local, $Dest, $Pass, $HostKey, [switch]$Quiet, [switch]$Recurse)
|
||||||
|
if ($pscp = Get-Command pscp.exe -ErrorAction SilentlyContinue) {
|
||||||
|
$arguments = @('-scp')
|
||||||
|
if ($Quiet) { $arguments += '-q' }
|
||||||
|
if ($Recurse) { $arguments += '-r' }
|
||||||
|
if ($HostKey) { $arguments += @('-batch', '-hostkey', $HostKey) }
|
||||||
|
if ($Pass) { $arguments += @('-pw', $Pass) }
|
||||||
|
$arguments += @($Local, $Dest)
|
||||||
|
Invoke-Ext -Exe $pscp.Path -Arguments $arguments -Label 'scp'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$arguments = @('-O')
|
||||||
|
if ($Quiet) { $arguments += '-q' }
|
||||||
|
if ($Recurse) { $arguments += '-r' }
|
||||||
|
$arguments += @($Local, $Dest)
|
||||||
|
Invoke-Ext -Exe 'scp' -Arguments $arguments -Label 'scp'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Ssh {
|
||||||
|
param($Cmd, $User, $Remote, $Pass, [switch]$Quiet)
|
||||||
|
$Cmd = $Cmd -replace "`r`n", "`n" -replace "`r", ""
|
||||||
|
if ($plink = Get-Command plink.exe -ErrorAction SilentlyContinue) {
|
||||||
|
$arguments = @('-batch', '-ssh')
|
||||||
|
if ($Pass) { $arguments += @('-pw', $Pass) }
|
||||||
|
$dest = if ($User) { "$User@$Remote" } else { $Remote }
|
||||||
|
$arguments += $dest, $Cmd
|
||||||
|
Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$arguments = @()
|
||||||
|
if ($Quiet) { $arguments += '-q' }
|
||||||
|
$dest = if ($User) { "$User@$Remote" } else { $Remote }
|
||||||
|
$arguments += $dest, $Cmd
|
||||||
|
Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
"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"
|
||||||
|
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
|
||||||
|
s = "".join(c if c.isalnum() else "_" for c in name)
|
||||||
|
if s and s[0].isdigit():
|
||||||
|
s = "_" + s
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def indent(text: str, n: int) -> str:
|
||||||
|
pad = " " * n
|
||||||
|
return textwrap.indent(text, pad)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
asset_root = RsAssetRoot(static)
|
||||||
|
|
||||||
|
# print(render_pain(asset_root))
|
||||||
|
# print(render_macro(asset_root))
|
||||||
|
|
||||||
|
# rs_code = render_pain(asset_root)
|
||||||
|
rs_code = render_macro(asset_root)
|
||||||
|
|
||||||
|
# generate
|
||||||
|
out_rs.write_text(rs_code, encoding="utf-8")
|
||||||
|
print(f"written to {out_rs.relative_to(Path.cwd(), walk_up=True)}")
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Package a release tarball with binaries and init scripts.
|
|
||||||
# Usage: ./scripts/package.ps1 -Version 0.1.0 -OutDir dist
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$Version,
|
|
||||||
[string]$OutDir = "dist",
|
|
||||||
[string[]]$Targets = @("armv7-unknown-linux-musleabihf")
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$root = Split-Path -Parent $MyInvocation.MyCommand.Path | Split-Path -Parent
|
|
||||||
$dist = Join-Path $root $OutDir
|
|
||||||
New-Item -ItemType Directory -Force -Path $dist | Out-Null
|
|
||||||
|
|
||||||
$pkgName = "wakey-$Version"
|
|
||||||
$pkgDir = Join-Path $dist $pkgName
|
|
||||||
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $pkgDir
|
|
||||||
New-Item -ItemType Directory -Force -Path $pkgDir | Out-Null
|
|
||||||
|
|
||||||
# Copy LICENSE/README if present
|
|
||||||
if (Test-Path "$root/README.md") { Copy-Item "$root/README.md" $pkgDir }
|
|
||||||
if (Test-Path "$root/LICENSE") { Copy-Item "$root/LICENSE" $pkgDir }
|
|
||||||
|
|
||||||
# Binaries per target
|
|
||||||
foreach ($t in $Targets) {
|
|
||||||
$binName = if ($t -like "*-windows-*") { "wakey.exe" } else { "wakey" }
|
|
||||||
$src = Join-Path $root "target/$t/release/$binName"
|
|
||||||
if (-not (Test-Path $src)) { throw "Missing binary: $src" }
|
|
||||||
$tDir = Join-Path $pkgDir "bin/$t"
|
|
||||||
New-Item -ItemType Directory -Force -Path $tDir | Out-Null
|
|
||||||
Copy-Item $src $tDir
|
|
||||||
}
|
|
||||||
|
|
||||||
# Init scripts
|
|
||||||
$initDir = Join-Path $pkgDir "init"
|
|
||||||
New-Item -ItemType Directory -Force -Path $initDir | Out-Null
|
|
||||||
Copy-Item "$root/scripts/init" $initDir -Recurse
|
|
||||||
|
|
||||||
# Systemd service (for Linux hosts)
|
|
||||||
Copy-Item "$root/scripts/systemd" $initDir -Recurse -ErrorAction SilentlyContinue
|
|
||||||
|
|
||||||
# Tarball
|
|
||||||
Push-Location $dist
|
|
||||||
if (Get-Command tar -ErrorAction SilentlyContinue) {
|
|
||||||
tar -czf "$pkgName.tgz" "$pkgName"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
# Fallback to zip on Windows
|
|
||||||
Compress-Archive -Path "$pkgName" -DestinationPath "$pkgName.zip" -Force
|
|
||||||
}
|
|
||||||
Pop-Location
|
|
||||||
|
|
||||||
Write-Host "Packaged: $dist/$pkgName.(tgz|zip)"
|
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$Version,
|
[Parameter(Mandatory = $true)][string]$Version,
|
||||||
[Parameter(Mandatory = $true)][string]$Target,
|
[Parameter(Mandatory = $true)][string]$Target,
|
||||||
[string]$OutDir = "dist"
|
[string]$OutDir = "dist",
|
||||||
|
[switch]$NoBin
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -17,7 +18,6 @@ New-Item -ItemType Directory -Force -Path $dist | Out-Null
|
|||||||
|
|
||||||
$binName = if ($Target -like "*-windows-*") { "wakey.exe" } else { "wakey" }
|
$binName = if ($Target -like "*-windows-*") { "wakey.exe" } else { "wakey" }
|
||||||
$binSrc = Join-Path $root "target/$Target/release/$binName"
|
$binSrc = Join-Path $root "target/$Target/release/$binName"
|
||||||
if (-not (Test-Path $binSrc)) { throw "Missing binary: $binSrc (build it first)" }
|
|
||||||
|
|
||||||
$staging = Join-Path $dist ("rootfs-" + [System.Guid]::NewGuid().ToString("N"))
|
$staging = Join-Path $dist ("rootfs-" + [System.Guid]::NewGuid().ToString("N"))
|
||||||
New-Item -ItemType Directory -Force -Path $staging | Out-Null
|
New-Item -ItemType Directory -Force -Path $staging | Out-Null
|
||||||
@@ -28,12 +28,43 @@ $etcDir = Join-Path $staging "etc/init.d"
|
|||||||
New-Item -ItemType Directory -Force -Path $rootDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $rootDir | Out-Null
|
||||||
New-Item -ItemType Directory -Force -Path $etcDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $etcDir | Out-Null
|
||||||
|
|
||||||
Copy-Item $binSrc (Join-Path $rootDir "wakey") -Force
|
if (-not $NoBin) {
|
||||||
|
if (Test-Path $binSrc) {
|
||||||
|
Copy-Item $binSrc (Join-Path $rootDir "wakey") -Force
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw "Missing binary: $binSrc (build it first or pass -NoBin)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# Normalize kill script line endings and copy
|
||||||
|
$killSrc = Join-Path $root 'scripts/kill_wakey.sh'
|
||||||
|
if (Test-Path $killSrc) {
|
||||||
|
$killContent = Get-Content -Raw -LiteralPath $killSrc
|
||||||
|
$killContent = $killContent -replace "`r`n", "`n"
|
||||||
|
Set-Content -NoNewline -LiteralPath (Join-Path $rootDir "kill_wakey.sh") -Value $killContent -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Normalize remote deploy script and copy (optional helper)
|
||||||
|
$deploySrc = Join-Path $root 'scripts/remote_deploy_wakey.sh'
|
||||||
|
if (Test-Path $deploySrc) {
|
||||||
|
$deployContent = Get-Content -Raw -LiteralPath $deploySrc
|
||||||
|
$deployContent = $deployContent -replace "`r`n", "`n"
|
||||||
|
Set-Content -NoNewline -LiteralPath (Join-Path $rootDir "remote_deploy_wakey.sh") -Value $deployContent -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy static assets
|
||||||
|
$staticSrc = Join-Path $root "static"
|
||||||
|
if (Test-Path $staticSrc) {
|
||||||
|
Copy-Item -Recurse $staticSrc (Join-Path $rootDir "static") -Force
|
||||||
|
}
|
||||||
|
|
||||||
# Copy all OpenWrt init scripts present in repo
|
# Copy all OpenWrt init scripts present in repo
|
||||||
Get-ChildItem (Join-Path $root 'scripts/init/openwrt') -File | ForEach-Object {
|
Get-ChildItem (Join-Path $root 'scripts/init/openwrt') -File | ForEach-Object {
|
||||||
$dest = Join-Path $etcDir $_.Name
|
$dest = Join-Path $etcDir $_.Name
|
||||||
Copy-Item $_.FullName $dest -Force
|
$content = Get-Content -Raw -LiteralPath $_.FullName
|
||||||
|
# normalize to LF line endings
|
||||||
|
$content = $content -replace "`r`n", "`n"
|
||||||
|
Set-Content -NoNewline -LiteralPath $dest -Value $content -Encoding UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
# Copy helper scripts intended for /etc/ldlda_help
|
# Copy helper scripts intended for /etc/ldlda_help
|
||||||
@@ -42,7 +73,10 @@ if (Test-Path $helpSrc) {
|
|||||||
$etcHelpDir = Join-Path $staging 'etc/ldlda_help'
|
$etcHelpDir = Join-Path $staging 'etc/ldlda_help'
|
||||||
New-Item -ItemType Directory -Force -Path $etcHelpDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $etcHelpDir | Out-Null
|
||||||
Get-ChildItem $helpSrc -File | ForEach-Object {
|
Get-ChildItem $helpSrc -File | ForEach-Object {
|
||||||
Copy-Item $_.FullName (Join-Path $etcHelpDir $_.Name) -Force
|
$d = Join-Path $etcHelpDir $_.Name
|
||||||
|
$c = Get-Content -Raw -LiteralPath $_.FullName
|
||||||
|
$c = $c -replace "`r`n", "`n"
|
||||||
|
Set-Content -NoNewline -LiteralPath $d -Value $c -Encoding UTF8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,4 +98,9 @@ Remove-Item -Recurse -Force $staging
|
|||||||
Write-Host "Rootfs package: " (Join-Path $dist $pkgName)
|
Write-Host "Rootfs package: " (Join-Path $dist $pkgName)
|
||||||
Write-Host "On router: wget -O- <URL/$pkgName> | tar -xz -C /"
|
Write-Host "On router: wget -O- <URL/$pkgName> | tar -xz -C /"
|
||||||
Write-Host "Then: chmod +x /etc/init.d/wakey && /etc/init.d/wakey enable && /etc/init.d/wakey start"
|
Write-Host "Then: chmod +x /etc/init.d/wakey && /etc/init.d/wakey enable && /etc/init.d/wakey start"
|
||||||
|
if ($NoBin) {
|
||||||
|
Write-Host "Note: -NoBin used, binary not included. Only scripts/configs were packaged." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
Write-Host "Helper scripts: /etc/ldlda_help/* (e.g., update_wakey.sh, update_tailscale.sh). Mark executable if needed: chmod +x /etc/ldlda_help/*.sh"
|
Write-Host "Helper scripts: /etc/ldlda_help/* (e.g., update_wakey.sh, update_tailscale.sh). Mark executable if needed: chmod +x /etc/ldlda_help/*.sh"
|
||||||
|
Write-Host "Kill helper: /root/.bin/kill_wakey.sh (make it executable: chmod +x /root/.bin/kill_wakey.sh)"
|
||||||
|
<# if (Test-Path $deploySrc) { #> Write-Host "Deploy helper: /root/.bin/remote_deploy_wakey.sh (chmod +x /root/.bin/remote_deploy_wakey.sh)" # }
|
||||||
+32
-4
@@ -9,20 +9,35 @@
|
|||||||
param(
|
param(
|
||||||
[string]$Version,
|
[string]$Version,
|
||||||
[switch]$Tag,
|
[switch]$Tag,
|
||||||
[switch]$Publish
|
[switch]$Publish,
|
||||||
[switch]$ForceTag,
|
|
||||||
[string]$Registry
|
[string]$Registry
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
if ($Version) {
|
# If no version provided, deduce from Cargo.toml
|
||||||
|
$versionProvided = [bool]$Version
|
||||||
|
if (-not $Version) {
|
||||||
|
$cargoToml = Get-Content Cargo.toml -Raw
|
||||||
|
if ($cargoToml -match 'version\s*=\s*"([^"]+)"') {
|
||||||
|
$Version = $matches[1]
|
||||||
|
Write-Host "Deduced version from Cargo.toml: $Version"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Error "Could not deduce version from Cargo.toml. Please specify -Version."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only write version if it was explicitly provided (not deduced)
|
||||||
|
if ($versionProvided) {
|
||||||
$pattern = '(?m)^version\s*=\s*"[^"]+"'
|
$pattern = '(?m)^version\s*=\s*"[^"]+"'
|
||||||
$replacement = ('version = "{0}"' -f $Version)
|
$replacement = ('version = "{0}"' -f $Version)
|
||||||
(Get-Content Cargo.toml -Raw) -replace $pattern, $replacement | Set-Content Cargo.toml -Encoding UTF8
|
(Get-Content Cargo.toml -Raw) -replace $pattern, $replacement | Set-Content Cargo.toml -Encoding UTF8 -NoNewline
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure build ok
|
# Ensure build ok
|
||||||
|
cargo fmt
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
# Publish crate (only if -Publish and registry auth is available)
|
# Publish crate (only if -Publish and registry auth is available)
|
||||||
@@ -32,6 +47,8 @@ if ($Publish) {
|
|||||||
$pubArgs = @('publish')
|
$pubArgs = @('publish')
|
||||||
if ($Registry) { $pubArgs += @('--registry', $Registry) }
|
if ($Registry) { $pubArgs += @('--registry', $Registry) }
|
||||||
cargo @pubArgs
|
cargo @pubArgs
|
||||||
|
|
||||||
|
Write-Host ("published. ran cargo {0}" -f ($pubArgs -join " "))
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Write-Warning ("cargo publish failed: {0}" -f $_)
|
Write-Warning ("cargo publish failed: {0}" -f $_)
|
||||||
@@ -41,4 +58,15 @@ if ($Publish) {
|
|||||||
if ($Tag -and $Version) {
|
if ($Tag -and $Version) {
|
||||||
git tag -f "v$Version"
|
git tag -f "v$Version"
|
||||||
git push -f origin "v$Version"
|
git push -f origin "v$Version"
|
||||||
|
Write-Host "Pushed tag: v$Version"
|
||||||
|
|
||||||
|
# Run act_runner in --once mode to process the release job
|
||||||
|
$actRunnerScript = Join-Path $PSScriptRoot 'act_runner.ps1'
|
||||||
|
if (Test-Path $actRunnerScript) {
|
||||||
|
Write-Host "Starting act_runner (once mode) to process release job..."
|
||||||
|
& $actRunnerScript -Once
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warning "act_runner.ps1 not found at $actRunnerScript. Skipping runner."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# One-shot: build, package, and print artifact location (no cross required).
|
|
||||||
# Usage: ./scripts/release.ps1 -Version 0.1.0 -Targets armv7-unknown-linux-musleabihf
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$Version,
|
|
||||||
[string[]]$Targets = @("armv7-unknown-linux-musleabihf")
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
foreach ($t in $Targets) {
|
|
||||||
rustup target add $t
|
|
||||||
cargo build --release --target $t
|
|
||||||
}
|
|
||||||
./scripts/package.ps1 -Version $Version -Targets $Targets
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Usage: remote_deploy_wakey.sh <bin_tmp> <dest_path> [restart_flag]
|
||||||
|
# restart_flag: 1 (default) to stop/start, 0 to only replace file.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
BIN_TMP="$1"
|
||||||
|
DEST="$2"
|
||||||
|
RESTART="${3:-1}"
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -f "$BIN_TMP" ] || {
|
||||||
|
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
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Wakey WOL+ARP service
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/bin/wakey
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=2
|
|
||||||
User=wakey
|
|
||||||
Group=wakey
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# Test ipjs on remote ARM device
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Package = "lda-ipjs",
|
||||||
|
[string]$TestName = "",
|
||||||
|
[ValidateSet("debug", "release")]
|
||||||
|
[string]$BuildProfile = "debug",
|
||||||
|
[string]$password,
|
||||||
|
[switch]$Verbose,
|
||||||
|
[string]$RemoteTestPath = "/root/.bin/test",
|
||||||
|
[string]$RemoteHost = "[email protected]"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
. "$PSScriptRoot/lib.ps1"
|
||||||
|
|
||||||
|
# Build tests and capture output
|
||||||
|
Write-Host "Building tests for $Package..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Stream cargo output anc convert to text
|
||||||
|
$cargoOutput = cargo test --no-run -p $Package --target armv7-unknown-linux-musleabihf $(if ($BuildProfile -eq "release") { "-r" }) 2>&1 |
|
||||||
|
ForEach-Object {
|
||||||
|
$line = if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } else { $_ }
|
||||||
|
if ($Verbose) {
|
||||||
|
Write-Host $line
|
||||||
|
}
|
||||||
|
$line # Pass through to capture
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse test binary paths from cargo output
|
||||||
|
$testBinaries = $cargoOutput |
|
||||||
|
Select-String -Pattern "Executable.*\((.+)\)" |
|
||||||
|
ForEach-Object { $_.Matches.Groups[1].Value } |
|
||||||
|
Get-Item
|
||||||
|
|
||||||
|
if ($testBinaries.Count -eq 0) {
|
||||||
|
Write-Error "No test binaries found! Exiting..."
|
||||||
|
# Write-Host "Cargo output:" -ForegroundColor Yellow
|
||||||
|
# $cargoOutput | ForEach-Object { Write-Host $_ }
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Found $($testBinaries.Count) test $($testBinaries.Count -eq 1 ? "binary" : "binaries")" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Run each test binary
|
||||||
|
foreach ($testBinary in $testBinaries) {
|
||||||
|
Write-Host "`nTesting: $($testBinary.Name)" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Copy to target
|
||||||
|
Invoke-Scp -Local $testBinary.FullName -Dest "${RemoteHost}:$RemoteTestPath" -Pass $password -Quiet
|
||||||
|
|
||||||
|
# Run on target
|
||||||
|
$testArgs = "$(if ($Verbose) {"--nocapture --show-output"})"
|
||||||
|
if ($TestName) {
|
||||||
|
$testArgs = "$TestName $testArgs"
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-Ssh -Cmd "chmod +x $RemoteTestPath && $RemoteTestPath $testArgs" -Remote $RemoteHost -Pass $password
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nDone!" -ForegroundColor Green
|
||||||
+3
-19
@@ -6,25 +6,9 @@ use thiserror::Error;
|
|||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
pub enum IPNeighParseError {
|
pub enum IPNeighParseError {
|
||||||
IpWhere, // i never seen a ip neigh where the first thing aint an ip
|
IpWhere, // i never seen a ip neigh where the first thing aint an ip
|
||||||
IpParseError(AddrParseError),
|
IpParseError(#[from] AddrParseError),
|
||||||
// DevWhere,
|
// DevWhere,
|
||||||
MacParseError(macaddr::ParseError),
|
MacParseError(#[from] macaddr::ParseError),
|
||||||
StateWhere, // i never seen a ip neigh without the big FAILED at the end
|
StateWhere, // i never seen a ip neigh without the big FAILED at the end
|
||||||
StateParseError(strum::ParseError),
|
StateParseError(#[from] strum::ParseError),
|
||||||
}
|
|
||||||
|
|
||||||
impl From<AddrParseError> for IPNeighParseError {
|
|
||||||
fn from(value: AddrParseError) -> Self {
|
|
||||||
Self::IpParseError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<macaddr::ParseError> for IPNeighParseError {
|
|
||||||
fn from(value: macaddr::ParseError) -> Self {
|
|
||||||
Self::MacParseError(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<strum::ParseError> for IPNeighParseError {
|
|
||||||
fn from(value: strum::ParseError) -> Self {
|
|
||||||
Self::StateParseError(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
use serde::{Deserialize, Deserializer, de};
|
|
||||||
|
|
||||||
use super::NUDState;
|
|
||||||
|
|
||||||
// Case-insensitive parsing for NUDState via manual Deserialize
|
|
||||||
impl<'de> Deserialize<'de> for NUDState {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s: &str = <&str as Deserialize>::deserialize(deserializer)?;
|
|
||||||
s.parse().map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! r#impl AHHHHHH
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer, de};
|
||||||
|
|
||||||
|
use super::NUDState;
|
||||||
|
|
||||||
|
// Case-insensitive parsing for NUDState via manual Deserialize
|
||||||
|
impl<'de> Deserialize<'de> for NUDState {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: &str = <&str as Deserialize>::deserialize(deserializer)?;
|
||||||
|
s.parse().map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use crate::arpparse::IpNeighLine;
|
||||||
|
|
||||||
|
use lda_ipjs::subcommands::neighbor::{self as ipjs_neigh, NeighborItem};
|
||||||
|
|
||||||
|
impl From<ipjs_neigh::NUDState> for NUDState {
|
||||||
|
fn from(value: ipjs_neigh::NUDState) -> Self {
|
||||||
|
match value {
|
||||||
|
ipjs_neigh::NUDState::Permanent => NUDState::Permanent,
|
||||||
|
ipjs_neigh::NUDState::Noarp => NUDState::Noarp,
|
||||||
|
ipjs_neigh::NUDState::Reachable => NUDState::Reachable,
|
||||||
|
ipjs_neigh::NUDState::Stale => NUDState::Stale,
|
||||||
|
ipjs_neigh::NUDState::None => NUDState::None,
|
||||||
|
ipjs_neigh::NUDState::Incomplete => NUDState::Incomplete,
|
||||||
|
ipjs_neigh::NUDState::Delay => NUDState::Delay,
|
||||||
|
ipjs_neigh::NUDState::Probe => NUDState::Probe,
|
||||||
|
ipjs_neigh::NUDState::Failed => NUDState::Failed,
|
||||||
|
ipjs_neigh::NUDState::Other(_) => NUDState::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NUDState> for ipjs_neigh::NUDState {
|
||||||
|
fn from(value: NUDState) -> Self {
|
||||||
|
match value {
|
||||||
|
NUDState::Permanent => ipjs_neigh::NUDState::Permanent,
|
||||||
|
NUDState::Noarp => ipjs_neigh::NUDState::Noarp,
|
||||||
|
NUDState::Reachable => ipjs_neigh::NUDState::Reachable,
|
||||||
|
NUDState::Stale => ipjs_neigh::NUDState::Stale,
|
||||||
|
NUDState::None => ipjs_neigh::NUDState::None,
|
||||||
|
NUDState::Incomplete => ipjs_neigh::NUDState::Incomplete,
|
||||||
|
NUDState::Delay => ipjs_neigh::NUDState::Delay,
|
||||||
|
NUDState::Probe => ipjs_neigh::NUDState::Probe,
|
||||||
|
NUDState::Failed => ipjs_neigh::NUDState::Failed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NeighborItem> for IpNeighLine {
|
||||||
|
fn from(
|
||||||
|
NeighborItem {
|
||||||
|
ip,
|
||||||
|
dev,
|
||||||
|
mac,
|
||||||
|
state,
|
||||||
|
}: NeighborItem,
|
||||||
|
) -> Self {
|
||||||
|
IpNeighLine {
|
||||||
|
ip,
|
||||||
|
dev,
|
||||||
|
mac,
|
||||||
|
state: state
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(NUDState::None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
|
|
||||||
use std::{net::IpAddr, str::FromStr};
|
use std::{net::IpAddr, str::FromStr};
|
||||||
|
|
||||||
|
use crate::utils::parse::mac;
|
||||||
use macaddr::MacAddr;
|
use macaddr::MacAddr;
|
||||||
use serde::{Deserialize, Serialize, Serializer, de};
|
|
||||||
use serde_with::skip_serializing_none;
|
use serde_with::skip_serializing_none;
|
||||||
use strum::{Display, EnumString};
|
use strum::{Display, EnumString};
|
||||||
|
|
||||||
use crate::arpparse::error::IPNeighParseError;
|
use crate::arpparse::error::IPNeighParseError;
|
||||||
mod error;
|
mod error;
|
||||||
mod r#impl; // custom (de)serialization impls
|
mod impls; // custom (de)serialization impls
|
||||||
|
|
||||||
/// ip neigh has some cool shit.
|
/// ip neigh has some cool shit.
|
||||||
///
|
///
|
||||||
@@ -33,31 +33,17 @@ pub struct IpNeighLine {
|
|||||||
pub ip: IpAddr,
|
pub ip: IpAddr,
|
||||||
pub dev: Option<String>,
|
pub dev: Option<String>,
|
||||||
/// link layer address
|
/// link layer address
|
||||||
#[serde(serialize_with = "ser_opm")]
|
#[serde(with = "mac::option_mac")]
|
||||||
pub mac: Option<MacAddr>,
|
pub mac: Option<MacAddr>,
|
||||||
/// Neighbour Unreachability Detection
|
/// Neighbour Unreachability Detection
|
||||||
pub state: NUDState,
|
pub state: NUDState,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// serialize an [`Option<MacAddr>`]
|
|
||||||
pub fn ser_opm<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
|
|
||||||
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// deserialize an [`Option<MacAddr>`]
|
|
||||||
pub fn _des_opm<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
Option::<&str>::deserialize(des)?
|
|
||||||
.map(str::parse)
|
|
||||||
.transpose()
|
|
||||||
.map_err(de::Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NUDState custom Deserialize now lives in arpparse/impl.rs; use serde_with OneOrMany for Vec
|
// NUDState custom Deserialize now lives in arpparse/impl.rs; use serde_with OneOrMany for Vec
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize)]
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize, Default,
|
||||||
|
)]
|
||||||
#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
|
#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
|
||||||
#[serde(rename_all = "UPPERCASE")]
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
pub enum NUDState {
|
pub enum NUDState {
|
||||||
@@ -79,10 +65,6 @@ pub enum NUDState {
|
|||||||
/// neighbour state if it was valid and the
|
/// neighbour state if it was valid and the
|
||||||
/// address is not changed by this command.
|
/// address is not changed by this command.
|
||||||
Stale,
|
Stale,
|
||||||
/// this is a pseudo state used when initially
|
|
||||||
/// creating a neighbour entry or after trying to
|
|
||||||
/// remove it before it becomes free to do so.
|
|
||||||
None,
|
|
||||||
|
|
||||||
/// the neighbour entry has not (yet) been
|
/// the neighbour entry has not (yet) been
|
||||||
/// validated/resolved.
|
/// validated/resolved.
|
||||||
@@ -96,6 +78,13 @@ pub enum NUDState {
|
|||||||
/// success, neighbor validation has ultimately
|
/// success, neighbor validation has ultimately
|
||||||
/// failed.
|
/// failed.
|
||||||
Failed,
|
Failed,
|
||||||
|
|
||||||
|
/// this is a pseudo state used when initially
|
||||||
|
/// creating a neighbour entry or after trying to
|
||||||
|
/// remove it before it becomes free to do so.
|
||||||
|
#[serde(other)]
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NUDState {
|
impl NUDState {
|
||||||
@@ -114,7 +103,7 @@ impl NUDState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// dumb UI label
|
/// dumb UI label
|
||||||
pub fn dumber_state(&self) -> &'static str {
|
pub fn _dumber_state(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
NUDState::Permanent | NUDState::Reachable => "online",
|
NUDState::Permanent | NUDState::Reachable => "online",
|
||||||
NUDState::Stale => "maybe online",
|
NUDState::Stale => "maybe online",
|
||||||
@@ -194,7 +183,7 @@ impl IpNeighLine {
|
|||||||
Self { state, ..self }
|
Self { state, ..self }
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
pub fn with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
|
pub fn _with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
|
||||||
let dev = dev.into();
|
let dev = dev.into();
|
||||||
move |self_| Self {
|
move |self_| Self {
|
||||||
dev: Some(dev.clone()),
|
dev: Some(dev.clone()),
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/// MAC->name cache location (ephemeral)
|
||||||
|
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
|
||||||
|
|
||||||
|
/// Load MAC->name cache from disk
|
||||||
|
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()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save MAC->name cache to disk
|
||||||
|
async fn save_mac_name_cache(map: &std::collections::BTreeMap<String, String>) -> io::Result<()> {
|
||||||
|
let s = serde_json::to_string(map).map_err(io::Error::other)?;
|
||||||
|
let _ = tokio::fs::write(MAC_NAME_CACHE, s).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read all leases, filling names from MAC->name cache if missing
|
||||||
|
pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLeaseLine>> {
|
||||||
|
let leases = read_dhcp_leases().await?;
|
||||||
|
let mut cache = load_mac_name_cache().await.unwrap_or_default();
|
||||||
|
let mut changed = false;
|
||||||
|
let mut leases_with_names = Vec::with_capacity(leases.len());
|
||||||
|
for mut l in leases {
|
||||||
|
let mac_s = l.mac.to_string();
|
||||||
|
if let Some(ref name) = l.name {
|
||||||
|
// if no name in cache file or it changed
|
||||||
|
if cache.get(&mac_s).map(|v| v != name).unwrap_or(true) {
|
||||||
|
cache.insert(mac_s, name.clone());
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
} else if let Some(prev) = cache.get(&mac_s) {
|
||||||
|
l.name = Some(prev.clone());
|
||||||
|
}
|
||||||
|
leases_with_names.push(l);
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
let _ = save_mac_name_cache(&cache).await;
|
||||||
|
}
|
||||||
|
Ok(leases_with_names)
|
||||||
|
}
|
||||||
|
use crate::utils::parse::mac;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use std::io::{self, ErrorKind};
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
/// A single line from /tmp/dhcp.leases
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct DhcpLeaseLine {
|
||||||
|
/// Epoch seconds when the lease expires
|
||||||
|
pub expires_epoch: u64,
|
||||||
|
pub ip: IpAddr,
|
||||||
|
#[serde(with = "mac")]
|
||||||
|
pub mac: MacAddr,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse one line of /tmp/dhcp.leases
|
||||||
|
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLeaseLine> {
|
||||||
|
let mut c = line.split_whitespace();
|
||||||
|
let expires_epoch: u64 = c.next()?.parse().ok()?;
|
||||||
|
let mac = c.next()?.parse().ok()?;
|
||||||
|
let ip = c.next()?.parse().ok()?;
|
||||||
|
let name = c.next().filter(|c| *c != "*").map(str::to_string);
|
||||||
|
// ignore any remaining columns (e.g., client-id)
|
||||||
|
Some(DhcpLeaseLine {
|
||||||
|
expires_epoch,
|
||||||
|
ip,
|
||||||
|
mac,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read all leases from /tmp/dhcp.leases (simple and fast)
|
||||||
|
pub async fn read_dhcp_leases() -> io::Result<Vec<DhcpLeaseLine>> {
|
||||||
|
match tokio::fs::read_to_string("/tmp/dhcp.leases").await {
|
||||||
|
Ok(file) => Ok(file.lines().filter_map(parse_dhcp_lease_line).collect()),
|
||||||
|
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Vec::new()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-104
@@ -1,115 +1,44 @@
|
|||||||
use axum::{
|
//! braindead version v0.1.x
|
||||||
Router,
|
//!
|
||||||
extract::Query,
|
//! # whats next
|
||||||
http::StatusCode,
|
//!
|
||||||
response::{Html, IntoResponse},
|
//! for version 2 i hope to have:
|
||||||
routing::{get, post},
|
//!
|
||||||
};
|
//! 1. idk reworked frontend;
|
||||||
|
//! 2. incorporate ip -j;
|
||||||
|
//! 3. small 1-5 second caching;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
mod arpparse;
|
mod arpparse;
|
||||||
|
mod dhcpparse;
|
||||||
mod route;
|
mod route;
|
||||||
mod r#static;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
use crate::{
|
use std::{env, io};
|
||||||
route::DeviceQuery,
|
|
||||||
utils::{ping::ping_ip, query::get_macs_2_1, wake::wake},
|
|
||||||
};
|
|
||||||
use r#static as st;
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
const MACHINE_NAME: &str = "lda.lan";
|
|
||||||
|
|
||||||
async fn home() -> Html<String> {
|
|
||||||
Html(format!(
|
|
||||||
r#"
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<p><a href="/home_2">Alternate UI</a></p>
|
|
||||||
<p>the machine is {}! <a href="/status">Status</a></p>
|
|
||||||
<form method="POST" action="/wake">
|
|
||||||
<button type="submit">Wake LDA</button>
|
|
||||||
</form>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"#,
|
|
||||||
if ping_ip((MACHINE_NAME, 22)).await {
|
|
||||||
"on"
|
|
||||||
} else {
|
|
||||||
"off"
|
|
||||||
} // match get_ips(MACHINE_NAME).await {
|
|
||||||
// Ok(ips) => {
|
|
||||||
// // let addrs: Vec<SocketAddr> = ips.into_iter().map(|ip|(ip, 22).into()).collect();
|
|
||||||
// }
|
|
||||||
// Err(_) => "off",
|
|
||||||
// }
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wake_handler(
|
|
||||||
Query(DeviceQuery { name, .. }): Query<DeviceQuery>,
|
|
||||||
) -> axum::response::Result<impl IntoResponse> {
|
|
||||||
match wake(name.as_deref().unwrap_or(MACHINE_NAME)).await {
|
|
||||||
Ok(0) => Err((StatusCode::NOT_FOUND, "No packets sent!").into()),
|
|
||||||
Ok(x) => Ok((
|
|
||||||
StatusCode::ACCEPTED,
|
|
||||||
format!("{x} packet{s} sent!", s = if x > 1 { "s" } else { "" }),
|
|
||||||
)),
|
|
||||||
_ => Err((StatusCode::GATEWAY_TIMEOUT, "Wake failed").into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn status_build(machine_name: &str) -> String {
|
|
||||||
let formatted_macs = match get_macs_2_1(machine_name).await {
|
|
||||||
Ok(table) => {
|
|
||||||
let the: String = table
|
|
||||||
.iter()
|
|
||||||
.map(|(ip, mac, state)| {
|
|
||||||
let mac_str = // if let Some(mac) = mac {
|
|
||||||
mac.to_string()
|
|
||||||
// } else {
|
|
||||||
// "None".into()
|
|
||||||
// }
|
|
||||||
;
|
|
||||||
format!(
|
|
||||||
"<tr><td>{ip}</td><td>{mac_str}</td><td>{state}</td></tr>",
|
|
||||||
state = state.dumber_state()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
format!(
|
|
||||||
r#"<p>info of {machine_name}:</p>
|
|
||||||
<table>
|
|
||||||
<tr><th>IP</th><th>MAC</th><th>State</th></tr>
|
|
||||||
{the}
|
|
||||||
</table>"#
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Err(e) => format!("<p>errors getting table for {machine_name}: {e}</p>"),
|
|
||||||
};
|
|
||||||
|
|
||||||
format!(
|
|
||||||
r#"
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
{formatted_macs}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn entry() -> io::Result<()> {
|
async fn entry() -> io::Result<()> {
|
||||||
use crate::route::{api_status, devs_router, get_status_2, home_2, home_2_route};
|
use crate::route::api_router;
|
||||||
|
use axum::routing::get_service;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
let exe = env::current_exe()?;
|
||||||
|
let root = exe
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| io::Error::other("no parent dir"))?;
|
||||||
|
let static_dir = ServeDir::new(root.join("static"))
|
||||||
|
.append_index_html_on_directories(true)
|
||||||
|
.precompressed_br()
|
||||||
|
.precompressed_deflate()
|
||||||
|
.precompressed_gzip()
|
||||||
|
.precompressed_zstd();
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/home", get(home))
|
// .route("/home", get(home))
|
||||||
.route("/", get(home_2))
|
// .route("/", get(home_2))
|
||||||
.merge(home_2_route())
|
// .merge(home_2_route())
|
||||||
.route("/wake", post(wake_handler))
|
// .route("/status", get(get_status_2))
|
||||||
.route("/status", get(get_status_2))
|
.nest("/api", api_router())
|
||||||
.merge(api_status())
|
.fallback_service(get_service(static_dir));
|
||||||
.route("/api/devs", get(devs_router));
|
|
||||||
|
|
||||||
let port = TcpListener::bind("0.0.0.0:12012").await?;
|
let port = TcpListener::bind("0.0.0.0:12012").await?;
|
||||||
axum::serve(port, app.into_make_service()).await?;
|
axum::serve(port, app.into_make_service()).await?;
|
||||||
@@ -118,8 +47,8 @@ async fn entry() -> io::Result<()> {
|
|||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
fn main() -> color_eyre::Result<()> {
|
fn main() -> color_eyre::Result<()> {
|
||||||
color_eyre::install()?;
|
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
|
color_eyre::install()?;
|
||||||
// use crate::arpparse::NUDState;
|
// use crate::arpparse::NUDState;
|
||||||
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
|
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
|
||||||
println!("{:?}", "svuhuvshdv:331".to_socket_addrs());
|
println!("{:?}", "svuhuvshdv:331".to_socket_addrs());
|
||||||
|
|||||||
-252
@@ -1,252 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::net::IpAddr;
|
|
||||||
|
|
||||||
use axum::{
|
|
||||||
Json, Router,
|
|
||||||
extract::Path,
|
|
||||||
http::{StatusCode, header},
|
|
||||||
response::{Html, IntoResponse, Redirect},
|
|
||||||
routing::get,
|
|
||||||
};
|
|
||||||
use axum_extra::extract::Query;
|
|
||||||
use macaddr::MacAddr;
|
|
||||||
use serde::ser::Serializer;
|
|
||||||
use serde_with::skip_serializing_none;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
MACHINE_NAME,
|
|
||||||
arpparse::{self, NUDState},
|
|
||||||
st, status_build,
|
|
||||||
utils::{
|
|
||||||
de_many,
|
|
||||||
query::{
|
|
||||||
dev::{self, has_dev},
|
|
||||||
get_macs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn home_2() -> Html<&'static str> {
|
|
||||||
Html(st::HOME_2)
|
|
||||||
}
|
|
||||||
async fn home_2_css() -> impl IntoResponse {
|
|
||||||
(
|
|
||||||
[
|
|
||||||
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
|
|
||||||
(header::CACHE_CONTROL, "public, max-age=300"),
|
|
||||||
],
|
|
||||||
st::HOME_2_CSS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
async fn home_2_js() -> impl IntoResponse {
|
|
||||||
(
|
|
||||||
[
|
|
||||||
(header::CONTENT_TYPE, "application/javascript"),
|
|
||||||
(header::CACHE_CONTROL, "public, max-age=300"),
|
|
||||||
],
|
|
||||||
st::HOME_2_JS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// all the pages related to home_2
|
|
||||||
pub fn home_2_route() -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route("/home_2", get(home_2))
|
|
||||||
.route("/home_2.css", get(home_2_css))
|
|
||||||
.route("/home_2.js", get(home_2_js)) //js
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
|
||||||
pub struct DeviceQuery {
|
|
||||||
pub name: Option<String>,
|
|
||||||
// Accept single or many; ignore blanks
|
|
||||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
|
||||||
ip: Vec<IpAddr>,
|
|
||||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
|
||||||
mac: Vec<MacAddr>,
|
|
||||||
/// optional interface filter (e.g., br-lan)
|
|
||||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
|
||||||
pub dev: Vec<String>,
|
|
||||||
/// optional NUD state filter; accepts any case (e.g., reachable, REACHABLE)
|
|
||||||
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
|
|
||||||
pub nud: Vec<NUDState>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Default, Clone, Hash, serde::Deserialize)]
|
|
||||||
pub struct NamePath {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Debug, Default, serde::Serialize)]
|
|
||||||
pub struct Status {
|
|
||||||
name: Option<String>,
|
|
||||||
table: Vec<arpparse::IpNeighLine>,
|
|
||||||
filters: Filters,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, serde::Serialize)]
|
|
||||||
pub struct Filters {
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
||||||
ip: Vec<IpAddr>,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
||||||
dev: Vec<String>,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
||||||
nud: Vec<NUDState>,
|
|
||||||
#[serde(
|
|
||||||
skip_serializing_if = "Vec::is_empty",
|
|
||||||
serialize_with = "serialize_macs"
|
|
||||||
)]
|
|
||||||
mac: Vec<MacAddr>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let strings: Vec<String> = macs.iter().map(|m| m.to_string()).collect();
|
|
||||||
serde::Serialize::serialize(&strings, serializer)
|
|
||||||
}
|
|
||||||
#[skip_serializing_none]
|
|
||||||
#[derive(Debug, serde::Serialize)]
|
|
||||||
pub struct StatusError {
|
|
||||||
name: Option<String>,
|
|
||||||
error: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_status_json(
|
|
||||||
// p: Option<Path<NamePath>>,
|
|
||||||
Query(DeviceQuery {
|
|
||||||
name,
|
|
||||||
ip,
|
|
||||||
dev,
|
|
||||||
nud,
|
|
||||||
mac,
|
|
||||||
..
|
|
||||||
}): Query<DeviceQuery>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> {
|
|
||||||
if slice.is_empty() {
|
|
||||||
vec![None]
|
|
||||||
} else {
|
|
||||||
slice.iter().cloned().map(Some).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* let name = /* p
|
|
||||||
.map(|Path(n)| n.name)
|
|
||||||
.or */(name)
|
|
||||||
// .unwrap_or_else(|| MACHINE_NAME.to_owned())
|
|
||||||
; */
|
|
||||||
// ip/dev/nud already parsed; assemble options
|
|
||||||
let ips_opt = if ip.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(ip.clone())
|
|
||||||
};
|
|
||||||
let dev_opts: Vec<Option<String>> = to_opts(&dev);
|
|
||||||
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
|
|
||||||
|
|
||||||
let filters = Filters {
|
|
||||||
ip,
|
|
||||||
dev,
|
|
||||||
nud,
|
|
||||||
mac: mac.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run combinations of dev/nud and merge results
|
|
||||||
let mut tasks = Vec::new();
|
|
||||||
for d in &dev_opts {
|
|
||||||
for n in &nud_opts {
|
|
||||||
tasks.push(get_macs(
|
|
||||||
name.as_deref(),
|
|
||||||
ips_opt.as_deref(),
|
|
||||||
d.as_deref(),
|
|
||||||
*n,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match futures::future::try_join_all(tasks)
|
|
||||||
.await
|
|
||||||
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
|
|
||||||
{
|
|
||||||
Ok(mut table) => {
|
|
||||||
// Optional MAC post-filtering if provided
|
|
||||||
if !mac.is_empty() {
|
|
||||||
let wanted: HashSet<MacAddr> = mac.into_iter().collect();
|
|
||||||
table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false));
|
|
||||||
}
|
|
||||||
// let canonical = format!("/api/status?name={name}");
|
|
||||||
(
|
|
||||||
StatusCode::OK,
|
|
||||||
// [(header::LINK, format!("<{canonical}>; rel=\"canonical\""))],
|
|
||||||
Json(Status {
|
|
||||||
name,
|
|
||||||
table,
|
|
||||||
filters,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_response()
|
|
||||||
}
|
|
||||||
Err(error) => (
|
|
||||||
StatusCode::BAD_GATEWAY,
|
|
||||||
Json(StatusError {
|
|
||||||
name,
|
|
||||||
error: error.to_string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_response(), // holy clutch. Couldve been disasterous
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
|
|
||||||
Redirect::permanent(&format!(
|
|
||||||
"/api/status?name={name}",
|
|
||||||
name = urlencoding::encode(&name) // just for
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn api_status() -> Router {
|
|
||||||
Router::new()
|
|
||||||
.route("/api/status/{name}", get(status_redirect)) // api/status should be like the entire ip neigh br lan like idk like
|
|
||||||
.route("/api/status", get(get_status_json))
|
|
||||||
.route("/api/smart/{q}", get(status_smart_redirect))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_status_2(q: Query<DeviceQuery>) -> Html<String> {
|
|
||||||
let name = match q {
|
|
||||||
Query(DeviceQuery {
|
|
||||||
name: Some(name), ..
|
|
||||||
}) => name,
|
|
||||||
_ => MACHINE_NAME.to_string(),
|
|
||||||
};
|
|
||||||
Html(status_build(&name).await)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly
|
|
||||||
pub async fn status_smart_redirect(Path(q): Path<String>) -> Redirect {
|
|
||||||
let s = q.trim();
|
|
||||||
// 1) IP
|
|
||||||
if let Ok(ip) = s.parse::<IpAddr>() {
|
|
||||||
return Redirect::to(&format!("/api/status?ip={ip}"));
|
|
||||||
}
|
|
||||||
// 2) MAC
|
|
||||||
if let Ok(mac) = s.parse::<MacAddr>() {
|
|
||||||
return Redirect::to(&format!("/api/status?mac={mac}"));
|
|
||||||
}
|
|
||||||
// 3) NUD state (reachable, stale, ...)
|
|
||||||
if let Ok(state) = s.parse::<NUDState>() {
|
|
||||||
return Redirect::to(&format!("/api/status?nud={state}"));
|
|
||||||
}
|
|
||||||
// 4) Known device? prefer dev first
|
|
||||||
if has_dev(s) {
|
|
||||||
return Redirect::to(&format!("/api/status?dev={}", urlencoding::encode(s)));
|
|
||||||
}
|
|
||||||
// 5) Try DNS: if it resolves, treat as name
|
|
||||||
if tokio::net::lookup_host((s, 0)).await.is_ok() {
|
|
||||||
return Redirect::to(&format!("/api/status?name={}", urlencoding::encode(s)));
|
|
||||||
}
|
|
||||||
// Default: name last
|
|
||||||
Redirect::to(&format!("/api/status?name={}", urlencoding::encode(s)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn devs_router() -> Json<Vec<String>> {
|
|
||||||
dev::devs_sorted().into()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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::{extract::Path, response::Redirect};
|
||||||
|
|
||||||
|
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> {
|
||||||
|
// 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 async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
|
||||||
|
Redirect::permanent(&format!(
|
||||||
|
"/api/status?name={name}",
|
||||||
|
name = urlencoding::encode(&name) // just for
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
use crate::utils::query::dev;
|
||||||
|
use axum::Json;
|
||||||
|
|
||||||
|
pub async fn devs_router() -> Json<Vec<String>> {
|
||||||
|
dev::devs_sorted().await.into()
|
||||||
|
}
|
||||||
|
// Device listing endpoints
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
|
#[derive(Debug, Default, Clone, serde::Deserialize)]
|
||||||
|
pub struct DhcpLeasesQueryRaw {
|
||||||
|
include_state: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl IntoResponse {
|
||||||
|
let include_state = raw
|
||||||
|
.include_state
|
||||||
|
.as_deref()
|
||||||
|
.map(boolish_str)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
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 = enrich_leases_with_nud_state(leases_with_names).await;
|
||||||
|
(StatusCode::OK, Json(out)).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,55 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod devs;
|
||||||
|
pub mod dhcp;
|
||||||
|
pub mod error;
|
||||||
|
pub mod status;
|
||||||
|
pub mod wake;
|
||||||
|
|
||||||
|
// use crate::assets;
|
||||||
|
use crate::dhcpparse::load_mac_name_cache;
|
||||||
|
use crate::route::api::ip;
|
||||||
|
use crate::route::api::status_redirect;
|
||||||
|
use crate::route::api::status_smart_redirect;
|
||||||
|
use crate::route::devs::devs_router;
|
||||||
|
use crate::route::dhcp::get_dhcp_leases;
|
||||||
|
use crate::route::status::get_status_json;
|
||||||
|
use crate::route::wake::wake_multi;
|
||||||
|
|
||||||
|
use axum::Json;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::body::Body;
|
||||||
|
use axum::http::Request;
|
||||||
|
use axum::middleware::{self, Next};
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::routing::{get, post};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
async fn add_performance_header(req: Request<Body>, next: Next) -> Response {
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut response = next.run(req).await;
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
|
if let Ok(val) = format!("work-time={}us", elapsed.as_micros()).parse() {
|
||||||
|
response.headers_mut().insert("Lda-Performance", val);
|
||||||
|
}
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn api_router() -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/status/{name}", get(status_redirect))
|
||||||
|
.route("/status", get(get_status_json))
|
||||||
|
.route("/dhcp_leases", get(get_dhcp_leases))
|
||||||
|
.route("/smart/{q}", get(status_smart_redirect))
|
||||||
|
.route("/devs", get(devs_router))
|
||||||
|
.route("/wake", post(wake_multi))
|
||||||
|
.route("/ips/{name}", get(ip))
|
||||||
|
.route(
|
||||||
|
"/mac-cache",
|
||||||
|
get(async || match load_mac_name_cache().await {
|
||||||
|
Ok(h) => Json(h).into_response(),
|
||||||
|
Err(e) => e.to_string().into_response(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.layer(middleware::from_fn(add_performance_header))
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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 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(flatten)]
|
||||||
|
pub filter: Filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Hash, Deserialize)]
|
||||||
|
pub struct NamePath {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct Status<T> {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub table: Vec<T>,
|
||||||
|
pub filters: Filters,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Filters {
|
||||||
|
#[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,
|
||||||
|
filter: filters,
|
||||||
|
..
|
||||||
|
}): Query<DeviceQuery>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match get_macs(
|
||||||
|
name.as_slice(),
|
||||||
|
&filters.ips,
|
||||||
|
&filters.devs,
|
||||||
|
&filters.nuds,
|
||||||
|
&filters.macs,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(table) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(Status {
|
||||||
|
name,
|
||||||
|
table,
|
||||||
|
filters,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(error) => ApiError {
|
||||||
|
code: StatusCode::BAD_GATEWAY,
|
||||||
|
error: error
|
||||||
|
.chain()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(": "),
|
||||||
|
}
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Status endpoints
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
//! impls are at [`utils::wake::impl`](crate::utils::wake::r#impl) for some reason
|
||||||
|
use std::io;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
/* use crate::arpparse::IpNeighLine;
|
||||||
|
use crate::route::api::Status; */
|
||||||
|
use crate::utils::parse::mac;
|
||||||
|
use crate::utils::wake::wake_one;
|
||||||
|
use axum::{extract::Json, http::StatusCode, response::IntoResponse};
|
||||||
|
use futures::TryFutureExt;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Default, Serialize, Clone)]
|
||||||
|
pub struct WakeResult {
|
||||||
|
pub success: bool,
|
||||||
|
pub result: Option<Vec<WakeTargetResult>>,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Clone, Copy)]
|
||||||
|
pub struct WakeTargetResult {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub target: WakeTarget,
|
||||||
|
pub status: WakeTargetStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone, Copy, Hash)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum WakeTargetStatus {
|
||||||
|
Succeed,
|
||||||
|
/// not a real address...
|
||||||
|
NonexistentAddress,
|
||||||
|
WrongSize,
|
||||||
|
/// input is not enough
|
||||||
|
Incomplete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||||
|
pub struct WakeTarget {
|
||||||
|
#[serde(default)]
|
||||||
|
pub ip: Option<IpAddr>,
|
||||||
|
#[serde(default, with = "mac::option_mac")]
|
||||||
|
pub mac: Option<MacAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wake_multi(Json(req): Json<Vec<WakeTarget>>) -> impl IntoResponse {
|
||||||
|
match wake_multi_split(req).await {
|
||||||
|
Ok(results) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(WakeResult {
|
||||||
|
success: true,
|
||||||
|
result: Some(results),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Err(error) => {
|
||||||
|
let error = Some(format!("Error: {}", error));
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(WakeResult {
|
||||||
|
success: false,
|
||||||
|
error,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// this is so bad
|
||||||
|
pub async fn wake_multi_split(
|
||||||
|
targets: impl IntoIterator<Item = WakeTarget>,
|
||||||
|
) -> io::Result<Vec<WakeTargetResult>> {
|
||||||
|
let sock = UdpSocket::bind("[::]:0")
|
||||||
|
.or_else(|_| UdpSocket::bind(":0"))
|
||||||
|
.await?;
|
||||||
|
sock.set_broadcast(true)?;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub status: IpNeighLine, // most powerful find of the century
|
||||||
|
pub wake_status: WakeTargetStatus,
|
||||||
|
}
|
||||||
|
pub type WakeStatus = Status<WakeStatusLine>; */
|
||||||
|
// /// return status BUT plus a indicator of i sent a wake.
|
||||||
|
// pub async fn wake_status(
|
||||||
|
// Query(DeviceQuery {
|
||||||
|
// name,
|
||||||
|
// ip,
|
||||||
|
// dev,
|
||||||
|
// nud,
|
||||||
|
// mac,
|
||||||
|
// ..
|
||||||
|
// }): Query<DeviceQuery>,
|
||||||
|
// ) -> impl IntoResponse {
|
||||||
|
// }
|
||||||
|
pub mod impls;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
use crate::utils::wake::{WakeStatus, WakeTarget, WakeTargetResult};
|
||||||
|
use crate::route::wake::{
|
||||||
|
WakeTarget as RouteWakeTarget, WakeTargetResult as RouteWakeResult,
|
||||||
|
WakeTargetStatus as RouteWakeStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Incomplete;
|
||||||
|
|
||||||
|
impl TryFrom<RouteWakeTarget> for WakeTarget {
|
||||||
|
type Error = Incomplete;
|
||||||
|
fn try_from(value: RouteWakeTarget) -> Result<Self, Self::Error> {
|
||||||
|
if let RouteWakeTarget {
|
||||||
|
ip: Some(ip),
|
||||||
|
mac: Some(mac),
|
||||||
|
} = value
|
||||||
|
{
|
||||||
|
Ok(Self { ip, mac })
|
||||||
|
} else {
|
||||||
|
Err(Incomplete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 const fn to_incomplete(self) -> RouteWakeResult {
|
||||||
|
RouteWakeResult {
|
||||||
|
target: self,
|
||||||
|
status: RouteWakeStatus::Incomplete,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn is_incomplete(&self) -> bool {
|
||||||
|
!matches!(
|
||||||
|
self,
|
||||||
|
Self {
|
||||||
|
ip: Some(_),
|
||||||
|
mac: Some(_)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<WakeStatus> for RouteWakeStatus {
|
||||||
|
fn from(value: WakeStatus) -> Self {
|
||||||
|
match value {
|
||||||
|
WakeStatus::NonexistentAddress => Self::NonexistentAddress,
|
||||||
|
WakeStatus::Success => Self::Succeed,
|
||||||
|
WakeStatus::WrongSize => Self::WrongSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub const HOME_2: &str = include_str!("../static/home_2.html");
|
|
||||||
pub const HOME_2_CSS: &str = include_str!("../static/assets/home_2.css");
|
|
||||||
pub const HOME_2_JS: &str = include_str!("../static/assets/home_2.js");
|
|
||||||
@@ -1,73 +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 std::net::IpAddr;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// this is because i like [`IpAddr`] more than [`SocketAddr`](std::net::SocketAddr)
|
|
||||||
pub async fn get_ips(machine_name: &str) -> error::Result<Vec<IpAddr>> {
|
|
||||||
let it = tokio::net::lookup_host((machine_name, 0))
|
|
||||||
.await
|
|
||||||
.map_err(|e| error::Error::DnsResolve {
|
|
||||||
name: machine_name.to_string(),
|
|
||||||
source: e,
|
|
||||||
})?;
|
|
||||||
Ok(it.map(|c| c.ip()).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod cmd;
|
|
||||||
pub mod query;
|
|
||||||
|
|
||||||
// no custom ip deserializer needed when using axum_extra::extract::Query
|
|
||||||
// but we add a generic one to ignore blanks and accept OneOrMany
|
|
||||||
pub 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,19 @@
|
|||||||
|
// pub const LDA_MACS: [[u8; 6]; 2] = [
|
||||||
|
// // ether
|
||||||
|
// [0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
|
||||||
|
// // wifi
|
||||||
|
// [0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
|
||||||
|
// ];
|
||||||
|
|
||||||
|
/// is it time to lookup host lda.lan for this...
|
||||||
|
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
|
||||||
|
pub mod wake;
|
||||||
|
|
||||||
|
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
|
||||||
|
// this is so bad
|
||||||
|
pub mod ping;
|
||||||
|
pub mod query;
|
||||||
|
|
||||||
|
// no custom ip deserializer needed when using axum_extra::extract::Query
|
||||||
|
// but we add a generic one to ignore blanks and accept OneOrMany
|
||||||
|
pub(crate) mod parse;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use macaddr::MacAddr;
|
||||||
|
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
|
||||||
|
use serde::{Serialize, Serializer, de};
|
||||||
|
|
||||||
|
pub fn _serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let strings: Vec<String> = macs.iter().map(|m| m.to_string()).collect();
|
||||||
|
serde::Serialize::serialize(&strings, serializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize a MacAddr as a string
|
||||||
|
pub fn serialize<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&mac.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize a MacAddr from a string
|
||||||
|
pub fn _deserialize<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||||
|
s.parse::<MacAddr>().map_err(DeError::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod option_mac {
|
||||||
|
use super::*;
|
||||||
|
/// serialize an [`Option<MacAddr>`]
|
||||||
|
pub fn serialize<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
|
||||||
|
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// deserialize an [`Option<MacAddr>`]
|
||||||
|
pub fn deserialize<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Option::<&str>::deserialize(des)?
|
||||||
|
.map(str::parse)
|
||||||
|
.transpose()
|
||||||
|
.map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/// Parses Chrome-style numeric IPv4 forms: hex (0x...), decimal, octal.
|
||||||
|
pub fn parse_numeric_ipv4(s: &str) -> Option<std::net::IpAddr> {
|
||||||
|
let s = s.trim();
|
||||||
|
// hex
|
||||||
|
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
|
||||||
|
&& hex.chars().all(|c| c.is_ascii_hexdigit())
|
||||||
|
&& let Ok(n) = u32::from_str_radix(hex, 16)
|
||||||
|
{
|
||||||
|
return Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(n)));
|
||||||
|
}
|
||||||
|
// decimal
|
||||||
|
if s.chars().all(|c| c.is_ascii_digit())
|
||||||
|
&& let Ok(n) = s.parse::<u32>()
|
||||||
|
{
|
||||||
|
return Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(n)));
|
||||||
|
}
|
||||||
|
// octal (leading 0, all octal digits)
|
||||||
|
if s.len() > 1
|
||||||
|
&& s.as_bytes()[0] == b'0'
|
||||||
|
&& s.chars().all(|c| matches!(c, '0'..='7'))
|
||||||
|
&& let Ok(n) = u32::from_str_radix(s, 8)
|
||||||
|
{
|
||||||
|
return Some(std::net::IpAddr::V4(std::net::Ipv4Addr::from(n)));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
/// Extracts the host portion from a URL-like string, for smart input parsing.
|
||||||
|
pub fn extract_host(input: &str) -> &str {
|
||||||
|
let mut s = input.trim();
|
||||||
|
// Strip scheme (e.g., http://, https://, ssh://) or network-path reference (//host)
|
||||||
|
if let Some(idx) = s.find("://") {
|
||||||
|
s = &s[idx + 3..];
|
||||||
|
} else if let Some(rest) = s.strip_prefix("//") {
|
||||||
|
s = rest;
|
||||||
|
}
|
||||||
|
// Strip potential userinfo (user@host)
|
||||||
|
if let Some((_, host)) = s.rsplit_once('@') {
|
||||||
|
s = host;
|
||||||
|
}
|
||||||
|
// If bracketed IPv6 like [::1]:8080/path -> extract inside brackets
|
||||||
|
if let Some(host) = s.strip_prefix('[') {
|
||||||
|
if let Some(end) = host.find(']') {
|
||||||
|
s = &host[..end];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Trim path suffix if any
|
||||||
|
if let Some(pos) = s.find('/') {
|
||||||
|
s = &s[..pos];
|
||||||
|
}
|
||||||
|
// Drop trailing :port if present and numeric, but only if there's exactly one ':'
|
||||||
|
if let Some((host, port)) = s.rsplit_once(':')
|
||||||
|
&& s.matches(':').count() == 1
|
||||||
|
&& port.chars().all(|c| c.is_ascii_digit())
|
||||||
|
{
|
||||||
|
s = host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// key for yes: "1" | "true" | "yes" | "on" | "y"
|
||||||
|
///
|
||||||
|
/// frfr
|
||||||
|
pub fn _de_boolish<'de, D>(des: D) -> Result<bool, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::Deserialize;
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum Boolish {
|
||||||
|
B(bool),
|
||||||
|
I(u8),
|
||||||
|
S(String),
|
||||||
|
}
|
||||||
|
Ok(match Boolish::deserialize(des)? {
|
||||||
|
Boolish::B(b) => b,
|
||||||
|
Boolish::I(i) => i != 0,
|
||||||
|
Boolish::S(s) => {
|
||||||
|
let t = s.trim().to_ascii_lowercase();
|
||||||
|
if t.is_empty() {
|
||||||
|
true // presence implies true
|
||||||
|
} else {
|
||||||
|
matches!(t.as_str(), "1" | "true" | "yes" | "on" | "y")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a tolerant boolean value from a string.
|
||||||
|
/// Accepts: "1", "true", "yes", "on", "y" as true; "0", "false", "no", "off", "n" as false.
|
||||||
|
/// Empty string means true (presence-only query flag).
|
||||||
|
pub fn boolish_str(s: &str) -> bool {
|
||||||
|
let t = s.trim().to_ascii_lowercase();
|
||||||
|
if t.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
matches!(t.as_str(), "1" | "true" | "yes" | "on" | "y")
|
||||||
|
|| (!matches!(t.as_str(), "0" | "false" | "no" | "off" | "n")
|
||||||
|
&& t.parse::<u64>().map(|n| n != 0).unwrap_or(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod mac;
|
||||||
+16
-2
@@ -1,13 +1,15 @@
|
|||||||
// this ENTIRE file is redundant... or?
|
// this ENTIRE file is redundant... or?
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::{net::IpAddr, time::Duration};
|
||||||
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
net::{TcpStream, ToSocketAddrs},
|
net::{TcpStream, ToSocketAddrs},
|
||||||
time::timeout,
|
time::timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
|
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))
|
timeout(Duration::from_secs(1), TcpStream::connect(addr))
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
@@ -15,3 +17,15 @@ pub async fn ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
|
|||||||
pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
|
pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
|
||||||
todo!("use icmp")
|
todo!("use icmp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 {
|
||||||
|
match get_mac(Some(addr.into()), None, &[] as &[NUDState]).await {
|
||||||
|
Err(_) => 0,
|
||||||
|
Ok(l) => l
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| e.state)
|
||||||
|
.max()
|
||||||
|
.map(NUDState::rank)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
use std::{collections::HashSet, net::IpAddr};
|
|
||||||
|
|
||||||
use macaddr::MacAddr;
|
|
||||||
// use tokio::io;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
arpparse::{self, IpNeighLine, NUDState},
|
|
||||||
utils::{
|
|
||||||
cmd::exec_command,
|
|
||||||
error::{self, Error, Result},
|
|
||||||
get_ips,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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.iter().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)
|
|
||||||
// .map(IpNeighLine::with_dev(dev)) // this could be after flatmap up there
|
|
||||||
.collect::<Vec<_>>())
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let res = futures::future::try_join_all(futures).await?; // async move block errs.
|
|
||||||
Ok(res
|
|
||||||
.into_iter()
|
|
||||||
.flatten() /* resolve double vec */
|
|
||||||
// .flatten() /* drop parse errors (flat_map cleared) */
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_macs(
|
|
||||||
machine_name: Option<&str>,
|
|
||||||
ips: Option<&[IpAddr]>,
|
|
||||||
dev: Option<&str>,
|
|
||||||
state: Option<NUDState>,
|
|
||||||
) -> Result<Vec<IpNeighLine>> {
|
|
||||||
// Collect IPs early (before any await) to avoid holding generics across await points
|
|
||||||
let ip_list: Option<Vec<IpAddr>> =
|
|
||||||
ips.map(|slice| slice.iter().copied().map(|ip| ip.to_canonical()).collect());
|
|
||||||
|
|
||||||
// Resolve by machine name if no IPs provided but we have a name
|
|
||||||
let ip_list = match (ip_list, machine_name) {
|
|
||||||
(Some(list), _) => list,
|
|
||||||
(None, Some(name)) => get_ips(name).await?.into_iter().collect(),
|
|
||||||
(None, None) => Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper to convert NUDState to the string expected by `ip neigh`
|
|
||||||
let nud_arg = state.map(NUDState::as_ip_neigh_arg);
|
|
||||||
// let nud_arg = Rc::new(state.map(|s| s.to_string().to_lowercase()));
|
|
||||||
// Build a closure to run one `ip neigh` invocation and parse results
|
|
||||||
let run_one = |to_ip: Option<IpAddr>| async move {
|
|
||||||
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
|
|
||||||
if let Some(ip) = to_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) = nud_arg {
|
|
||||||
args.push("nud".into());
|
|
||||||
args.push(nud.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let o = exec_command("ip", args.iter().map(String::as_str).collect::<Vec<_>>()).await?; // hope to rustc that it knows how to unfuck ts
|
|
||||||
if !o.status.success() {
|
|
||||||
return Err(Error::CommandFailed {
|
|
||||||
cmd: "ip",
|
|
||||||
args,
|
|
||||||
status: o.status.code(),
|
|
||||||
stderr: String::from_utf8_lossy(&o.stderr).into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let lines = String::from_utf8_lossy(&o.stdout);
|
|
||||||
// Parse lines and, if a specific dev filter was used, stamp that dev onto rows
|
|
||||||
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::<Vec<IpNeighLine>, error::Error>(rows)
|
|
||||||
};
|
|
||||||
|
|
||||||
// If we have specific IPs, query each; otherwise query the whole table once
|
|
||||||
if !ip_list.is_empty() {
|
|
||||||
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip)));
|
|
||||||
let res = futures::future::try_join_all(futures).await?;
|
|
||||||
Ok(res.into_iter().flatten().collect())
|
|
||||||
} else {
|
|
||||||
run_one(None).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod dev {
|
|
||||||
use std::{collections::HashSet, sync::LazyLock};
|
|
||||||
|
|
||||||
pub fn get_dev() -> HashSet<String> {
|
|
||||||
// Prefer /sys/class/net, fallback to /proc/net/dev; filter out loopback
|
|
||||||
let mut devs: HashSet<String> = HashSet::new();
|
|
||||||
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
|
|
||||||
for e in rd.flatten() {
|
|
||||||
if 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) {
|
|
||||||
// skip headers
|
|
||||||
if let Some((name, _rest)) = line.split_once(':') {
|
|
||||||
let n = name.trim().to_string();
|
|
||||||
if n != "lo" && !n.is_empty() {
|
|
||||||
devs.insert(n);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
devs
|
|
||||||
}
|
|
||||||
|
|
||||||
pub static DEVS: LazyLock<HashSet<String>> = LazyLock::new(get_dev);
|
|
||||||
|
|
||||||
pub fn devs_sorted() -> Vec<String> {
|
|
||||||
let mut v: Vec<String> = DEVS.iter().cloned().collect();
|
|
||||||
v.sort();
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_dev(name: &str) -> bool {
|
|
||||||
DEVS.contains(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
// /// 50ms
|
||||||
|
// pub async fn get_dev() -> HashSet<String> {
|
||||||
|
// use lda_ipjs::subcommands::address::json as ipjs_json;
|
||||||
|
// let mut devs: HashSet<String> = HashSet::new();
|
||||||
|
// if let Ok(items) = ipjs_json::get(None).await {
|
||||||
|
// for item in items {
|
||||||
|
// if item.ifname != "lo" && !item.ifname.is_empty() {
|
||||||
|
// devs.insert(item.ifname);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// devs
|
||||||
|
// }
|
||||||
|
|
||||||
|
/// 3ms
|
||||||
|
pub async fn get_dev() -> HashSet<String> {
|
||||||
|
use std::fs;
|
||||||
|
fn get_dev() -> HashSet<String> {
|
||||||
|
let mut devs: HashSet<String> = HashSet::new();
|
||||||
|
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
|
||||||
|
for e in rd.flatten() {
|
||||||
|
if e.file_type()
|
||||||
|
.map(|ft| {
|
||||||
|
if ft.is_symlink() {
|
||||||
|
// true
|
||||||
|
fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false)
|
||||||
|
} else {
|
||||||
|
ft.is_dir()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
&& let Ok(name) = e.file_name().into_string()
|
||||||
|
&& name != "lo"
|
||||||
|
&& !name.is_empty()
|
||||||
|
{
|
||||||
|
devs.insert(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
|
||||||
|
for line in txt.lines().skip(2) {
|
||||||
|
if let Some((name, _rest)) = line.split_once(':') {
|
||||||
|
let n = name.trim().to_string();
|
||||||
|
if n != "lo" && !n.is_empty() {
|
||||||
|
devs.insert(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
devs
|
||||||
|
}
|
||||||
|
tokio::task::spawn_blocking(get_dev)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn devs_sorted() -> Vec<String> {
|
||||||
|
let mut v: Vec<String> = get_dev().await.into_iter().collect();
|
||||||
|
v.sort();
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn has_dev(name: &str) -> bool {
|
||||||
|
get_dev().await.contains(name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use crate::arpparse::NUDState;
|
||||||
|
use crate::dhcpparse::DhcpLeaseLine;
|
||||||
|
use crate::utils::query::get_macs;
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct DhcpLeaseOut {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub lease_line: DhcpLeaseLine,
|
||||||
|
pub nud_state: Option<NUDState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enrich DHCP leases with NUD state and rank using get_macs
|
||||||
|
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
|
||||||
|
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
|
||||||
|
let mut map: std::collections::HashMap<IpAddr, NUDState> = std::collections::HashMap::new();
|
||||||
|
if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await {
|
||||||
|
for row in rows {
|
||||||
|
let state = row.state;
|
||||||
|
let r = state.rank();
|
||||||
|
map.entry(row.ip)
|
||||||
|
.and_modify(|e| {
|
||||||
|
let er = e.rank();
|
||||||
|
if r > er {
|
||||||
|
*e = state
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_insert(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
leases
|
||||||
|
.into_iter()
|
||||||
|
.map(|lease_line| {
|
||||||
|
let nud_state = map.get(&lease_line.ip).copied();
|
||||||
|
DhcpLeaseOut {
|
||||||
|
lease_line,
|
||||||
|
nud_state,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
use lda_ipjs::subcommands::neighbor;
|
||||||
|
use macaddr::MacAddr;
|
||||||
|
|
||||||
|
use crate::arpparse::{IpNeighLine, NUDState};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
||||||
|
Ok(tokio::net::lookup_host((machine_name, 0))
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("DNS resolve failed for {machine_name}"))?
|
||||||
|
.map(|c| c.ip()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query neighbor table with multi-filters. Empty slice = no filter.
|
||||||
|
pub async fn get_macs(
|
||||||
|
machine_names: &[impl AsRef<str>],
|
||||||
|
ips: &[IpAddr],
|
||||||
|
devs: &[impl AsRef<str>],
|
||||||
|
state: &[NUDState],
|
||||||
|
macs: &[MacAddr],
|
||||||
|
) -> Result<Vec<IpNeighLine>> {
|
||||||
|
// Resolve machine names to IPs
|
||||||
|
let resolved_ips: HashSet<IpAddr> = if !machine_names.is_empty() {
|
||||||
|
futures::future::try_join_all(machine_names.iter().map(|n| get_ips(n.as_ref())))
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
HashSet::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge provided IPs with resolved IPs
|
||||||
|
let ip_filter: Vec<IpAddr> = if ips.is_empty() && resolved_ips.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else if ips.is_empty() {
|
||||||
|
resolved_ips.into_iter().collect()
|
||||||
|
} else if resolved_ips.is_empty() {
|
||||||
|
ips.iter().map(|ip| ip.to_canonical()).collect()
|
||||||
|
} else {
|
||||||
|
// Intersection: only IPs that appear in both
|
||||||
|
ips.iter()
|
||||||
|
.map(|ip| ip.to_canonical())
|
||||||
|
.filter(|ip| resolved_ips.contains(ip))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert state filter
|
||||||
|
let nud_filter: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
|
||||||
|
|
||||||
|
// Convert devs to &str for nl::get
|
||||||
|
let dev_strs: Vec<&str> = devs.iter().map(AsRef::as_ref).collect();
|
||||||
|
|
||||||
|
// Single rtnetlink call with all filters
|
||||||
|
let results: Vec<IpNeighLine> = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
|
||||||
|
.await
|
||||||
|
.context("rtnetlink failed")?
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy single-filter wrapper. Use get_macs for multi-filter.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub async fn get_mac(
|
||||||
|
ip: Option<IpAddr>,
|
||||||
|
dev: Option<&str>,
|
||||||
|
state: &[NUDState],
|
||||||
|
) -> Result<Vec<IpNeighLine>> {
|
||||||
|
let ips: Vec<IpAddr> = ip.into_iter().collect();
|
||||||
|
let devs: Vec<&str> = dev.into_iter().collect();
|
||||||
|
let nud: Vec<neighbor::NUDState> = state.iter().copied().map(Into::into).collect();
|
||||||
|
|
||||||
|
Ok(neighbor::nl::get(&ips, &devs, &nud, &[])
|
||||||
|
.await
|
||||||
|
.context("rtnetlink failed")?
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod dev;
|
||||||
|
pub mod leases;
|
||||||
|
pub mod macs;
|
||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
pub use leases::*;
|
||||||
|
pub use macs::*;
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
+69
-27
@@ -1,32 +1,74 @@
|
|||||||
|
//! why did my Head Ass split these into two.
|
||||||
|
|
||||||
use std::{io, net::IpAddr};
|
use std::{io, net::IpAddr};
|
||||||
|
|
||||||
|
use futures::TryFutureExt;
|
||||||
|
use macaddr::MacAddr;
|
||||||
use tokio::net::UdpSocket;
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
use crate::utils::query::get_macs_2_mac;
|
#[derive(Debug, Clone, Copy, Hash)]
|
||||||
|
pub struct WakeTarget {
|
||||||
pub async fn wake(machine_name: &str) -> io::Result<u32> {
|
pub ip: IpAddr,
|
||||||
let suh = UdpSocket::bind("0.0.0.0:0").await?;
|
pub mac: MacAddr,
|
||||||
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 WakeTargetResult {
|
||||||
|
pub target: WakeTarget,
|
||||||
|
pub status: WakeStatus,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy, Hash)]
|
||||||
|
pub enum WakeStatus {
|
||||||
|
Success,
|
||||||
|
NonexistentAddress,
|
||||||
|
WrongSize,
|
||||||
|
}
|
||||||
|
impl WakeTarget {
|
||||||
|
const fn _new(ip: IpAddr, mac: MacAddr) -> Self {
|
||||||
|
Self { ip, mac }
|
||||||
|
}
|
||||||
|
const fn good(self) -> WakeTargetResult {
|
||||||
|
WakeTargetResult::new(self, WakeStatus::Success)
|
||||||
|
}
|
||||||
|
const fn bad(self) -> WakeTargetResult {
|
||||||
|
WakeTargetResult::new(self, WakeStatus::WrongSize)
|
||||||
|
}
|
||||||
|
const fn errored(self) -> WakeTargetResult {
|
||||||
|
WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl WakeTargetResult {
|
||||||
|
const fn new(target: WakeTarget, status: WakeStatus) -> Self {
|
||||||
|
Self { target, status }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// its time. we have the ip; the macs. we dont need to send to the uh the broadcast anymore???
|
||||||
|
pub async fn _wake_multi(
|
||||||
|
targets: impl IntoIterator<Item = WakeTarget>,
|
||||||
|
) -> io::Result<Vec<WakeTargetResult>> {
|
||||||
|
let sock = UdpSocket::bind("[::]:0")
|
||||||
|
.or_else(|_| UdpSocket::bind(":0"))
|
||||||
|
.await?;
|
||||||
|
sock.set_broadcast(true)?;
|
||||||
|
let fs = targets.into_iter().map(|t| wake_one(&sock, t));
|
||||||
|
Ok(futures::future::join_all(fs).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wake_one(sock: &UdpSocket, t: WakeTarget) -> WakeTargetResult {
|
||||||
|
let mac = t.mac;
|
||||||
|
let mb = mac.as_bytes();
|
||||||
|
let mut pac = [0; 6 + 6 * 16];
|
||||||
|
pac[..6].fill(0xff);
|
||||||
|
for i in 1..=16 {
|
||||||
|
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
|
||||||
|
}
|
||||||
|
let ip = t.ip;
|
||||||
|
let port = 9;
|
||||||
|
match sock.send_to(&pac, (ip, port)).await {
|
||||||
|
Ok(n) if n == pac.len() => t.good(),
|
||||||
|
Ok(_) => t.bad(),
|
||||||
|
Err(_) => t.errored(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub async fn wake_query();
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
const qs = new URLSearchParams(location.search);
|
|
||||||
const $ = (id) => document.getElementById(id);
|
|
||||||
const elName = $("name");
|
|
||||||
const elCheck = $("check");
|
|
||||||
const elWake = $("wake");
|
|
||||||
const elLog = $("log");
|
|
||||||
const elHtml = $("html");
|
|
||||||
const pill = $("status-pill");
|
|
||||||
const link = $("permalink");
|
|
||||||
|
|
||||||
function setPill(kind, text) {
|
|
||||||
pill.className = `pill ${kind}`;
|
|
||||||
pill.textContent = text;
|
|
||||||
}
|
|
||||||
function setLink(name) {
|
|
||||||
const url = new URL(location.href);
|
|
||||||
if (name) url.searchParams.set("name", name);
|
|
||||||
else url.searchParams.delete("name");
|
|
||||||
history.replaceState(null, "", url);
|
|
||||||
link.href = url.toString();
|
|
||||||
}
|
|
||||||
function getName() {
|
|
||||||
return (elName.value || "").trim();
|
|
||||||
}
|
|
||||||
function saveName(name) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem("wakey:name", name);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
function loadName() {
|
|
||||||
return qs.get("name") || localStorage.getItem("wakey:name") || "";
|
|
||||||
}
|
|
||||||
/** @param {String} s NUD state */
|
|
||||||
function rankState(s) {
|
|
||||||
const key = String(s || "")
|
|
||||||
.trim()
|
|
||||||
.toUpperCase();
|
|
||||||
return (
|
|
||||||
{
|
|
||||||
PERMANENT: 5,
|
|
||||||
REACHABLE: 5,
|
|
||||||
STALE: 4,
|
|
||||||
DELAY: 3,
|
|
||||||
PROBE: 3,
|
|
||||||
INCOMPLETE: 3,
|
|
||||||
NOARP: 2,
|
|
||||||
NONE: 1,
|
|
||||||
FAILED: 0,
|
|
||||||
}[key] ?? 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildStatusUrl(name) {
|
|
||||||
const u = new URL("/api/status", location.origin);
|
|
||||||
if (name) u.searchParams.set("name", name);
|
|
||||||
// forward multi-value filters from current page URL
|
|
||||||
for (const k of ["ip", "dev", "nud", "mac"]) {
|
|
||||||
const vals = qs.getAll(k);
|
|
||||||
for (const v of vals) u.searchParams.append(k, v);
|
|
||||||
}
|
|
||||||
return u;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {{ name?: String, table: Array<{ ip, dev, mac, state }>, filters: {ip, dev, nud, mac}} | { name?: String, error }} data from /api/status */
|
|
||||||
function renderStatus(data) {
|
|
||||||
const tbl = document.createElement("table");
|
|
||||||
tbl.innerHTML = `<tr><th>IP</th><th>MAC</th><th>State</th><th>IF</th></tr>`;
|
|
||||||
for (const row of data.table || []) {
|
|
||||||
const tr = document.createElement("tr");
|
|
||||||
const mac = row.mac ?? "";
|
|
||||||
const dev = row.dev ?? "";
|
|
||||||
tr.innerHTML = `<td>${row.ip}</td><td>${mac}</td><td>${row.state}</td><td>${dev}</td>`;
|
|
||||||
tbl.appendChild(tr);
|
|
||||||
}
|
|
||||||
elHtml.innerHTML = "";
|
|
||||||
// Optional: show applied filters
|
|
||||||
if (data.filters) {
|
|
||||||
const parts = [];
|
|
||||||
if (Array.isArray(data.filters.ip) && data.filters.ip.length)
|
|
||||||
parts.push(`ip=[${data.filters.ip.join(", ")}]`);
|
|
||||||
if (Array.isArray(data.filters.dev) && data.filters.dev.length)
|
|
||||||
parts.push(`dev=[${data.filters.dev.join(", ")}]`);
|
|
||||||
if (Array.isArray(data.filters.nud) && data.filters.nud.length)
|
|
||||||
parts.push(`nud=[${data.filters.nud.join(", ")}]`);
|
|
||||||
if (Array.isArray(data.filters.mac) && data.filters.mac.length)
|
|
||||||
parts.push(`mac=[${data.filters.mac.join(", ")}]`);
|
|
||||||
if (parts.length) {
|
|
||||||
const info = document.createElement("div");
|
|
||||||
info.className = "filters";
|
|
||||||
info.textContent = `Filters: ${parts.join("; ")}`;
|
|
||||||
elHtml.appendChild(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elHtml.appendChild(tbl);
|
|
||||||
|
|
||||||
if ((data.table || []).length > 0) {
|
|
||||||
const best = data.table.reduce((a, b) =>
|
|
||||||
rankState(b.state) > rankState(a.state) ? b : a
|
|
||||||
);
|
|
||||||
const r = rankState(best.state);
|
|
||||||
if (r >= 5) setPill("ok", "online");
|
|
||||||
else if (r >= 2) setPill("warn", "maybe");
|
|
||||||
else setPill("bad", "offline");
|
|
||||||
} else {
|
|
||||||
setPill("warn", "unknown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchStatus(name) {
|
|
||||||
setPill("warn", "checking…");
|
|
||||||
const u = buildStatusUrl(name);
|
|
||||||
elLog.textContent = "GET " + u.pathname + u.search;
|
|
||||||
elHtml.innerHTML = "";
|
|
||||||
try {
|
|
||||||
const r = await fetch(u);
|
|
||||||
if (!r.ok) {
|
|
||||||
let msg = String(r.status);
|
|
||||||
try {
|
|
||||||
const err = await r.clone().json();
|
|
||||||
msg = err.error || JSON.stringify(err);
|
|
||||||
} catch {
|
|
||||||
msg = await r.text();
|
|
||||||
}
|
|
||||||
elLog.textContent = `status error: ${msg}`;
|
|
||||||
// elHtml.textContent = msg;
|
|
||||||
setPill("bad", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await r.json();
|
|
||||||
renderStatus(data);
|
|
||||||
} catch (e) {
|
|
||||||
elLog.textContent = "status error: " + e;
|
|
||||||
setPill("bad", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendWake(name) {
|
|
||||||
setPill("warn", "waking…");
|
|
||||||
elLog.textContent = "POST /wake?name=" + name;
|
|
||||||
try {
|
|
||||||
const r = await fetch(`/wake?name=${encodeURIComponent(name)}`, {
|
|
||||||
method: "POST",
|
|
||||||
// headers: { "X-Target-Name": name },
|
|
||||||
});
|
|
||||||
const t = await r.text();
|
|
||||||
elLog.textContent = t || "ok";
|
|
||||||
// Optional: recheck after medium delay
|
|
||||||
setTimeout(() => fetchStatus(name), 1500);
|
|
||||||
} catch (e) {
|
|
||||||
elLog.textContent = "wake error: " + e;
|
|
||||||
setPill("bad", "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
elCheck.addEventListener("click", () => {
|
|
||||||
const name = getName();
|
|
||||||
if (!name) return;
|
|
||||||
saveName(name);
|
|
||||||
setLink(name);
|
|
||||||
fetchStatus(name);
|
|
||||||
});
|
|
||||||
elWake.addEventListener("click", () => {
|
|
||||||
const name = getName();
|
|
||||||
if (!name) return;
|
|
||||||
saveName(name);
|
|
||||||
setLink(name);
|
|
||||||
sendWake(name);
|
|
||||||
});
|
|
||||||
elName.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Enter") elCheck.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// init
|
|
||||||
const initial = loadName();
|
|
||||||
if (initial) {
|
|
||||||
elName.value = initial;
|
|
||||||
setLink(initial); // when the permalink doing YOUR job
|
|
||||||
// auto-check on load in this A/B page
|
|
||||||
fetchStatus(initial);
|
|
||||||
} else {
|
|
||||||
setPill("warn", "unknown");
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// import { filter_array } from "./status.js";
|
||||||
|
|
||||||
|
export const qs = new URLSearchParams(location.search);
|
||||||
|
|
||||||
|
// $ is normally queryselector are we fr
|
||||||
|
export const $ = (id) => document.getElementById(id);
|
||||||
|
export const elName = $("name");
|
||||||
|
export const elCheck = $("check");
|
||||||
|
export const elWake = $("wake");
|
||||||
|
export const elLog = $("log");
|
||||||
|
export const elHtml = $("html");
|
||||||
|
export const elLeases = $("leases_html");
|
||||||
|
export const pill = $("status-pill");
|
||||||
|
export const link = $("permalink");
|
||||||
|
export const elPreview = $("preview");
|
||||||
|
|
||||||
|
export function setPill(kind, text) {
|
||||||
|
pill.className = `pill ${kind}`;
|
||||||
|
pill.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLink(name, clear) {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
if (clear) url.search = ""; // yo
|
||||||
|
/* const hasExtraFilters = filter_array.some(
|
||||||
|
(k) => url.searchParams.getAll(k).length
|
||||||
|
);
|
||||||
|
if (hasExtraFilters) {
|
||||||
|
// jus returns whatever; they are specifying further
|
||||||
|
} else */ {
|
||||||
|
if (name) url.searchParams.set("name", name);
|
||||||
|
else url.searchParams.delete("name");
|
||||||
|
}
|
||||||
|
history.replaceState(null, "", url);
|
||||||
|
link.href = url.toString();
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { elLeases } from "./dom.js";
|
||||||
|
import { rankState } from "./utils.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {{
|
||||||
|
* expires_epoch: Number
|
||||||
|
* rank?: Number
|
||||||
|
* ip: String
|
||||||
|
* mac: String
|
||||||
|
* nud_state?: String
|
||||||
|
* name?: String
|
||||||
|
* }[]} leases
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function renderLeases(leases) {
|
||||||
|
if (!elLeases) return;
|
||||||
|
if (!Array.isArray(leases) || leases.length === 0) {
|
||||||
|
elLeases.textContent = "no leases";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tbl = document.createElement("table");
|
||||||
|
tbl.className = "table";
|
||||||
|
|
||||||
|
(
|
||||||
|
tbl.tHead || tbl.createTHead()
|
||||||
|
).innerHTML = `<tr><th></th><th>IP</th><th>MAC</th><th>Name</th><th>Expires</th></tr>`;
|
||||||
|
|
||||||
|
const tbd = tbl.tBodies.item(0) || tbl.createTBody();
|
||||||
|
const nowSec = Math.floor(Date.now() / 1000);
|
||||||
|
for (const l of leases) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
const exp = Number(l.expires_epoch || 0);
|
||||||
|
const expired = exp > 0 && exp <= nowSec;
|
||||||
|
const when = exp > 0 ? new Date(exp * 1000) : null;
|
||||||
|
const whenText = when ? when.toLocaleString() : "";
|
||||||
|
let dotClass = "dot ok";
|
||||||
|
if (expired) {
|
||||||
|
dotClass = "dot bad";
|
||||||
|
} else if (l?.nud_state) {
|
||||||
|
if (rankState(l.nud_state) >= 5) dotClass = "dot ok";
|
||||||
|
else if (rankState(l.nud_state) >= 2) dotClass = "dot warn";
|
||||||
|
else dotClass = "dot bad";
|
||||||
|
} else if (typeof l?.rank === "number") {
|
||||||
|
if (l.rank >= 5) dotClass = "dot ok";
|
||||||
|
else if (l.rank >= 2) dotClass = "dot warn";
|
||||||
|
else dotClass = "dot bad";
|
||||||
|
}
|
||||||
|
const title = expired
|
||||||
|
? "expired"
|
||||||
|
: l?.nud_state
|
||||||
|
? String(l.nud_state).toLowerCase()
|
||||||
|
: "unknown";
|
||||||
|
const ip = l.ip || "";
|
||||||
|
const mac = l.mac || "";
|
||||||
|
const name = l.name || "";
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td><span class="${dotClass}" title="${title}"></span></td>
|
||||||
|
${Object.entries({ ip, mac, name })
|
||||||
|
.map(
|
||||||
|
([name, value]) =>
|
||||||
|
`<td>${
|
||||||
|
value &&
|
||||||
|
`<a href="#" class="pick" data-value="${value}" title="filter by ${name}">${value}</a>`
|
||||||
|
}</td>`
|
||||||
|
)
|
||||||
|
.join("\n")}
|
||||||
|
<td><span class="tiny">${whenText}</span></td>`;
|
||||||
|
tbd.appendChild(tr);
|
||||||
|
}
|
||||||
|
elLeases.innerHTML = "";
|
||||||
|
elLeases.appendChild(tbl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchLeases() {
|
||||||
|
try {
|
||||||
|
const r = await fetch("/api/dhcp_leases?include_state=1");
|
||||||
|
if (!r.ok) return;
|
||||||
|
const leases = await r.json();
|
||||||
|
renderLeases(leases);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
elName,
|
||||||
|
elCheck,
|
||||||
|
elWake,
|
||||||
|
elHtml,
|
||||||
|
elLeases,
|
||||||
|
elPreview,
|
||||||
|
setLink,
|
||||||
|
setPill,
|
||||||
|
qs,
|
||||||
|
} from "./dom.js";
|
||||||
|
import {
|
||||||
|
getName,
|
||||||
|
saveName,
|
||||||
|
loadName,
|
||||||
|
extractHostLikeBackend,
|
||||||
|
} from "./utils.js";
|
||||||
|
import { fetchStatus } from "./status.js";
|
||||||
|
import { fetchLeases } from "./leases.js";
|
||||||
|
import { sendWake } from "./wake.js";
|
||||||
|
|
||||||
|
// delegated click handler
|
||||||
|
function handlePickClick(e) {
|
||||||
|
const a = e.target && e.target.closest("a.pick");
|
||||||
|
if (!a) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const v = a.getAttribute("data-value");
|
||||||
|
pickTarget(v);
|
||||||
|
}
|
||||||
|
if (elHtml) elHtml.addEventListener("click", handlePickClick);
|
||||||
|
if (elLeases) elLeases.addEventListener("click", handlePickClick);
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
const raw = getName(elName);
|
||||||
|
const host = extractHostLikeBackend(raw);
|
||||||
|
elPreview.textContent = host && host !== raw ? `→ ${host}` : "";
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickTarget(value) {
|
||||||
|
const v = String(value || "").trim();
|
||||||
|
if (!v) return;
|
||||||
|
elName.value = v;
|
||||||
|
updatePreview();
|
||||||
|
saveName(v);
|
||||||
|
setLink(v, true);
|
||||||
|
fetchStatus(v);
|
||||||
|
fetchLeases();
|
||||||
|
}
|
||||||
|
|
||||||
|
// events
|
||||||
|
elCheck.addEventListener("click", () => {
|
||||||
|
const name = getName(elName);
|
||||||
|
if (!name) return;
|
||||||
|
saveName(name);
|
||||||
|
setLink(name);
|
||||||
|
fetchStatus(name);
|
||||||
|
fetchLeases();
|
||||||
|
});
|
||||||
|
elWake.addEventListener("click", () => {
|
||||||
|
const name = getName(elName);
|
||||||
|
if (!name) return;
|
||||||
|
saveName(name);
|
||||||
|
setLink(name);
|
||||||
|
sendWake(name);
|
||||||
|
fetchLeases();
|
||||||
|
});
|
||||||
|
elName.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter") elCheck.click();
|
||||||
|
});
|
||||||
|
elName.addEventListener("input", updatePreview);
|
||||||
|
|
||||||
|
// init
|
||||||
|
const initial = loadName(qs);
|
||||||
|
if (initial) {
|
||||||
|
elName.value = initial;
|
||||||
|
updatePreview();
|
||||||
|
if (qs.has("name")) {
|
||||||
|
setLink(initial);
|
||||||
|
fetchStatus(initial);
|
||||||
|
} else {
|
||||||
|
fetchStatus();
|
||||||
|
}
|
||||||
|
fetchLeases();
|
||||||
|
} else {
|
||||||
|
setPill("warn", "unknown");
|
||||||
|
updatePreview();
|
||||||
|
fetchStatus();
|
||||||
|
fetchLeases();
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
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",
|
||||||
|
state: "state",
|
||||||
|
dev: "interface",
|
||||||
|
};
|
||||||
|
export const status_array = Object.keys(status_map);
|
||||||
|
// 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) {
|
||||||
|
return new URL(`/api/smart/${encodeURIComponent(name)}`, location.origin);
|
||||||
|
}
|
||||||
|
const u = new URL("/api/status", location.origin);
|
||||||
|
if (name) u.searchParams.set("name", name);
|
||||||
|
for (const k of filter_array) {
|
||||||
|
const vals = qs.getAll(k);
|
||||||
|
for (const v of vals) u.searchParams.append(k, v);
|
||||||
|
}
|
||||||
|
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";
|
||||||
|
tbl.innerHTML = `<thead><tr>${
|
||||||
|
data.has_wake ? "<th>Wake status</th>" : ""
|
||||||
|
}<th>IP</th><th>MAC</th><th>State</th><th>IF</th></tr></thead>`;
|
||||||
|
|
||||||
|
// sum hax
|
||||||
|
const tbd = tbl.tBodies.item(0) || tbl.createTBody();
|
||||||
|
for (const row of data.table) {
|
||||||
|
if (data.has_wake && !row.wake_status)
|
||||||
|
throw TypeError("specified has wake but no wake stats");
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.innerHTML = `${
|
||||||
|
row.wake_status
|
||||||
|
? `<td><span class="dot ${
|
||||||
|
row.wake_status == "succeed" ? "ok" : "bad"
|
||||||
|
}" title="${translate_wake_message(row.wake_status)}"></span></td>`
|
||||||
|
: ""
|
||||||
|
}${Object.entries(status_map)
|
||||||
|
.map(([field, description]) => {
|
||||||
|
const value = row[field];
|
||||||
|
return `<td>${
|
||||||
|
value
|
||||||
|
? `<a href="#" class="pick" data-value="${value}" title="filter by ${description}">${value}</a>`
|
||||||
|
: ""
|
||||||
|
}</td>`;
|
||||||
|
})
|
||||||
|
.join("")}`;
|
||||||
|
tbd.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
elHtml.innerHTML = "";
|
||||||
|
if (data.filters) {
|
||||||
|
const parts = [];
|
||||||
|
filter_array.forEach((field) => {
|
||||||
|
if (Array.isArray(data.filters[field]) && data.filters[field].length)
|
||||||
|
parts.push(`${field}=[${data.filters[field].join(", ")}]`);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parts.length) {
|
||||||
|
const info = document.createElement("div");
|
||||||
|
info.className = "filters";
|
||||||
|
info.textContent = `Filters: ${parts.join("; ")}`;
|
||||||
|
elHtml.appendChild(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elHtml.appendChild(tbl);
|
||||||
|
|
||||||
|
if ((data.table || []).length > 0) {
|
||||||
|
const best = data.table.reduce((a, b) =>
|
||||||
|
rankState(b.state) > rankState(a.state) ? b : a
|
||||||
|
);
|
||||||
|
const r = rankState(best.state);
|
||||||
|
if (r >= 5) setPill("ok", "online");
|
||||||
|
else if (r >= 2) setPill("warn", "maybe");
|
||||||
|
else setPill("bad", "offline");
|
||||||
|
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);
|
||||||
|
elLog.textContent = "GET " + u.pathname + u.search;
|
||||||
|
try {
|
||||||
|
const r = await fetch(u);
|
||||||
|
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 {
|
||||||
|
msg = await r.text();
|
||||||
|
}
|
||||||
|
elLog.textContent = `status error: ${msg}`;
|
||||||
|
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) {
|
||||||
|
data.table = merge_wake_data(data.table, data_wake);
|
||||||
|
data.has_wake = true;
|
||||||
|
}
|
||||||
|
if (render) renderStatus(data);
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
elLog.textContent = "status error: " + e;
|
||||||
|
setPill("bad", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@ body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
wrap {
|
||||||
|
margin-inline: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
@@ -111,6 +118,13 @@ pre {
|
|||||||
a {
|
a {
|
||||||
color: #93c5fd;
|
color: #93c5fd;
|
||||||
}
|
}
|
||||||
|
.table a.pick {
|
||||||
|
color: #93c5fd;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.table a.pick:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
#html {
|
#html {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -126,3 +140,43 @@ a {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* simple table styling */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid #2a2a2a;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.table th {
|
||||||
|
text-align: left;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tiny {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--muted);
|
||||||
|
}
|
||||||
|
.dot.ok {
|
||||||
|
background: var(--ok);
|
||||||
|
}
|
||||||
|
.dot.bad {
|
||||||
|
background: var(--bad);
|
||||||
|
}
|
||||||
|
.dot.warn {
|
||||||
|
background: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
export function getName(elName) {
|
||||||
|
return (elName.value || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveName(name) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem("wakey:name", name);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadName(qs) {
|
||||||
|
return qs.get("name") || localStorage.getItem("wakey:name") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractHostLikeBackend(input) {
|
||||||
|
let s = String(input || "").trim();
|
||||||
|
if (!s) return "";
|
||||||
|
const schemeIdx = s.indexOf("://");
|
||||||
|
if (schemeIdx >= 0) s = s.slice(schemeIdx + 3);
|
||||||
|
else if (s.startsWith("//")) s = s.slice(2);
|
||||||
|
const at = s.lastIndexOf("@");
|
||||||
|
if (at >= 0) s = s.slice(at + 1);
|
||||||
|
if (s.startsWith("[")) {
|
||||||
|
const end = s.indexOf("]");
|
||||||
|
if (end > 1) s = s.slice(1, end);
|
||||||
|
} else {
|
||||||
|
const slash = s.indexOf("/");
|
||||||
|
if (slash >= 0) s = s.slice(0, slash);
|
||||||
|
const colon = s.lastIndexOf(":");
|
||||||
|
if (colon > 0 && (s.match(/:/g) || []).length === 1) {
|
||||||
|
const port = s.slice(colon + 1);
|
||||||
|
if (/^\d+$/.test(port)) s = s.slice(0, colon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rankState(s) {
|
||||||
|
const key = String(s || "")
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
PERMANENT: 5,
|
||||||
|
REACHABLE: 5,
|
||||||
|
STALE: 4,
|
||||||
|
DELAY: 3,
|
||||||
|
PROBE: 3,
|
||||||
|
INCOMPLETE: 3,
|
||||||
|
NOARP: 2,
|
||||||
|
NONE: 1,
|
||||||
|
FAILED: 0,
|
||||||
|
}[key] ?? 0
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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.
|
||||||
|
const wake_targets = data.table.map(({ ip, mac }) => {
|
||||||
|
return { ip, mac };
|
||||||
|
});
|
||||||
|
setPill("warn", "waking…");
|
||||||
|
elLog.textContent = "POST /api/wake";
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/wake`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(wake_targets),
|
||||||
|
});
|
||||||
|
if (!r.ok) {
|
||||||
|
let msg = String(r.status);
|
||||||
|
try {
|
||||||
|
const err = await r.clone().json();
|
||||||
|
msg = err.error || JSON.stringify(err);
|
||||||
|
} catch {
|
||||||
|
msg = await r.text();
|
||||||
|
}
|
||||||
|
elLog.textContent = `wake error: ${msg}`;
|
||||||
|
setPill("bad", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const j = await r.json();
|
||||||
|
|
||||||
|
if (!j.success) {
|
||||||
|
elLog.textContent = `wake error: ${j.error}`;
|
||||||
|
setPill("bad", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = j.result;
|
||||||
|
data.table = merge_wake_data(data.table, result);
|
||||||
|
data.has_wake = true;
|
||||||
|
|
||||||
|
renderStatus(data);
|
||||||
|
|
||||||
|
setTimeout(() => fetchStatus(name, undefined, result), 2000); // long ass timeout
|
||||||
|
} catch (e) {
|
||||||
|
elLog.textContent = "wake error: " + e;
|
||||||
|
setPill("bad", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
if (wake.length != table.length)
|
||||||
|
throw TypeError(
|
||||||
|
"wake status table and status table not of the same length"
|
||||||
|
);
|
||||||
|
const return_array = [];
|
||||||
|
let linear_failed = false;
|
||||||
|
for (const [index, entry] of table.entries()) {
|
||||||
|
if (wake[index].ip != entry.ip || wake[index].mac != entry.mac) {
|
||||||
|
linear_failed = true;
|
||||||
|
break;
|
||||||
|
} // use alternative method
|
||||||
|
|
||||||
|
return_array.push({ wake_status: wake[index].status, ...entry });
|
||||||
|
}
|
||||||
|
|
||||||
|
// never happening AHH
|
||||||
|
if (linear_failed) {
|
||||||
|
return_array = [];
|
||||||
|
const wake_map = new Map();
|
||||||
|
wake.forEach(({ ip, mac, status }) => {
|
||||||
|
wake_map.set(JSON.stringify({ ip, mac }), status);
|
||||||
|
});
|
||||||
|
return_array = table.map((entry) => {
|
||||||
|
const { ip, mac } = entry;
|
||||||
|
return {
|
||||||
|
wake_status: wake_map.get(JSON.stringify({ ip, mac })),
|
||||||
|
...entry,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//
|
||||||
|
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":
|
||||||
|
return "Incomplete address (both ip and MAC required)"
|
||||||
|
case "succeed":
|
||||||
|
return "Wake request sent successfully"
|
||||||
|
case "nonexistent_address":
|
||||||
|
return "Errored pinging this address (nonexistent address)"
|
||||||
|
case "wrong_size":
|
||||||
|
return "Wake request malformed"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>wakey • home_2</title>
|
<title>wakey • home</title>
|
||||||
<link rel="stylesheet" href="/home_2.css" />
|
<link rel="stylesheet" href="home_2/styles.css" />
|
||||||
<script type="module" src="/home_2.js" defer></script>
|
<script type="module" src="home_2/main.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<header>
|
<header>
|
||||||
<h1>home_2 <span class="muted">A/B test page</span></h1>
|
<h1>home <span class="muted">ping your device with a WoL</span></h1>
|
||||||
<span id="status-pill" class="pill warn">unknown</span>
|
<span id="status-pill" class="pill warn">unknown</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<input
|
<input
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="target name e.g. lda.lan"
|
placeholder="target (name, ip, mac...)"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
<button id="check" class="secondary">Check</button>
|
<button id="check" class="secondary">Check</button>
|
||||||
@@ -26,17 +26,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row muted" style="gap: 16px">
|
<div class="row muted" style="gap: 16px">
|
||||||
<span
|
<span
|
||||||
>Uses query <span title="available keys: name, ip, dev, nud">(?name=...)</span> on this page to view the
|
>Uses query
|
||||||
status.<!-- and header (X-Target-Name) so either extractor
|
<span title="available keys: name, ips, macs, devs, nuds"
|
||||||
path works. --></span
|
>(?name=...)</span
|
||||||
>
|
>
|
||||||
<a id="permalink" href="#">permalink</a>
|
on this page to view the status.</span
|
||||||
|
><span id="preview" class="tiny"></span
|
||||||
|
><a id="permalink" href="#">permalink</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section id="out" class="card">
|
<section id="out" class="card">
|
||||||
<pre id="log">ready.</pre>
|
<pre id="log">ready.</pre>
|
||||||
<div id="html"></div>
|
<div id="html"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="leases" class="card">
|
||||||
|
<h3 style="margin-top: 0">DHCP leases</h3>
|
||||||
|
<div id="leases_html"></div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in new issue
Block a user