2 Commits
24 changed files with 161 additions and 249 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
{
"rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf",
"rust-analyzer.diagnostics.disabled": ["unlinked-file"]
// "rust-analyzer.diagnostics.disabled": ["unlinked-file"]
}
Generated
+1
View File
@@ -1312,6 +1312,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
name = "wakey"
version = "0.1.6"
dependencies = [
"anyhow",
"axum",
"axum-extra",
"color-eyre",
+1
View File
@@ -5,6 +5,7 @@ edition = "2024"
publish = ["gitea"]
[dependencies]
anyhow = "1.0.100"
axum = { version = "0.8.4", features = ["macros"] }
axum-extra = { version = "0.10.1", features = ["query"] }
color-eyre = "0.6.5"
+15 -8
View File
@@ -19,17 +19,14 @@
// 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 mut cmd = tokio::process::Command::new("ip");
cmd.args(["-j", "address", "show"]);
if let Some(d) = dev {
cmd.args(["dev", d]);
}
let output = cmd.output().await?;
let output = _get(dev).await.context("Can not run command")?;
if !output.status.success() {
anyhow::bail!(String::from_utf8_lossy(&output.stderr).into_owned());
@@ -37,3 +34,13 @@ pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
Ok(serde_json::from_slice(&output.stdout)?)
}
pub async fn _get(dev: Option<&str>) -> io::Result<Output> {
let mut cmd = tokio::process::Command::new("ip");
cmd.args(["-j", "address", "show"]);
if let Some(d) = dev {
cmd.args(["dev", d]);
}
cmd.output().await
}
+13 -9
View File
@@ -1,8 +1,8 @@
//! idk what to put here
use std::net::IpAddr;
use std::{io, net::IpAddr, process::Output};
use anyhow::bail;
use anyhow::{Context, bail};
use super::{NUDState, NeighborItem};
@@ -19,6 +19,16 @@ pub async fn get(
dev: Option<&str>,
nud: &[NUDState],
) -> anyhow::Result<Vec<NeighborItem>> {
let output = _get(ip, dev, nud).await.context("Can not run command")?;
if !output.status.success() {
bail!(String::from_utf8_lossy(&output.stderr).into_owned())
} else {
Ok(serde_json::from_slice(&output.stdout)?)
}
}
pub async fn _get(ip: Option<IpAddr>, dev: Option<&str>, nud: &[NUDState]) -> io::Result<Output> {
let mut cmd = tokio::process::Command::new("ip");
cmd.args(["-j", "neigh", "show"]);
@@ -34,11 +44,5 @@ pub async fn get(
cmd.arg(nud.to_string());
}
let output = cmd.output().await?;
if !output.status.success() {
bail!(String::from_utf8_lossy(&output.stderr).into_owned())
} else {
Ok(serde_json::from_slice(&output.stdout)?)
}
cmd.output().await
}
+1 -1
View File
@@ -65,7 +65,7 @@ pub enum NUDState {
/// failed.
Failed,
Other(u16)
Other(u16),
}
/// everything i see
+1 -1
View File
@@ -1,2 +1,2 @@
pub mod mac;
pub mod vec;
// pub mod vec;
+1 -1
View File
@@ -8,7 +8,7 @@ enum OneOrMany<T> {
Many(Vec<T>),
}
pub fn vec_from_strs<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
pub fn deserialize<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
+52 -2
View File
@@ -15,5 +15,55 @@ impl<'de> Deserialize<'de> for NUDState {
}
}
#[allow(unused_imports)]
pub use crate::utils::parse::mac::{des_opm, ser_opm};
use crate::arpparse::IpNeighLine;
use lda_ipjs::subcommands::neighbor::{self as ipjs_neigh, NeighborItem};
impl From<ipjs_neigh::NUDState> for NUDState {
fn from(value: ipjs_neigh::NUDState) -> Self {
match value {
ipjs_neigh::NUDState::Permanent => NUDState::Permanent,
ipjs_neigh::NUDState::Noarp => NUDState::Noarp,
ipjs_neigh::NUDState::Reachable => NUDState::Reachable,
ipjs_neigh::NUDState::Stale => NUDState::Stale,
ipjs_neigh::NUDState::None => NUDState::None,
ipjs_neigh::NUDState::Incomplete => NUDState::Incomplete,
ipjs_neigh::NUDState::Delay => NUDState::Delay,
ipjs_neigh::NUDState::Probe => NUDState::Probe,
ipjs_neigh::NUDState::Failed => NUDState::Failed,
ipjs_neigh::NUDState::Other(_) => NUDState::None,
}
}
}
impl From<NUDState> for ipjs_neigh::NUDState {
fn from(value: NUDState) -> Self {
match value {
NUDState::Permanent => ipjs_neigh::NUDState::Permanent,
NUDState::Noarp => ipjs_neigh::NUDState::Noarp,
NUDState::Reachable => ipjs_neigh::NUDState::Reachable,
NUDState::Stale => ipjs_neigh::NUDState::Stale,
NUDState::None => ipjs_neigh::NUDState::None,
NUDState::Incomplete => ipjs_neigh::NUDState::Incomplete,
NUDState::Delay => ipjs_neigh::NUDState::Delay,
NUDState::Probe => ipjs_neigh::NUDState::Probe,
NUDState::Failed => ipjs_neigh::NUDState::Failed,
}
}
}
impl From<NeighborItem> for IpNeighLine {
fn from(item: NeighborItem) -> Self {
IpNeighLine {
ip: item.ip,
dev: Some(item.dev),
mac: item.mac,
state: item
.state
.first()
.copied()
.map(Into::into)
.unwrap_or(NUDState::None),
}
}
}
+7 -6
View File
@@ -10,7 +10,7 @@
use std::{net::IpAddr, str::FromStr};
use impls::ser_opm;
use crate::utils::parse::mac;
use macaddr::MacAddr;
use serde_with::skip_serializing_none;
use strum::{Display, EnumString};
@@ -33,7 +33,7 @@ pub struct IpNeighLine {
pub ip: IpAddr,
pub dev: Option<String>,
/// link layer address
#[serde(serialize_with = "ser_opm")]
#[serde(with = "mac::option_mac")]
pub mac: Option<MacAddr>,
/// Neighbour Unreachability Detection
pub state: NUDState,
@@ -41,7 +41,9 @@ pub struct IpNeighLine {
// NUDState custom Deserialize now lives in arpparse/impl.rs; use serde_with OneOrMany for Vec
#[derive(Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize, Default)]
#[derive(
Debug, PartialEq, Eq, EnumString, Display, Clone, Copy, Hash, serde::Serialize, Default,
)]
#[strum(serialize_all = "UPPERCASE", ascii_case_insensitive)]
#[serde(rename_all = "UPPERCASE")]
pub enum NUDState {
@@ -64,7 +66,6 @@ pub enum NUDState {
/// address is not changed by this command.
Stale,
/// the neighbour entry has not (yet) been
/// validated/resolved.
Incomplete,
@@ -88,7 +89,7 @@ pub enum NUDState {
impl NUDState {
/// Argument form expected by `ip neigh ... nud <state>` (lowercase)
pub const fn as_ip_neigh_arg(self) -> &'static str {
pub const fn _as_ip_neigh_arg(self) -> &'static str {
match self {
NUDState::Permanent => "permanent",
NUDState::Reachable => "reachable",
@@ -182,7 +183,7 @@ impl IpNeighLine {
Self { state, ..self }
}
*/
pub fn with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
pub fn _with_dev(dev: impl Into<String>) -> impl FnMut(Self) -> Self {
let dev = dev.into();
move |self_| Self {
dev: Some(dev.clone()),
+2 -6
View File
@@ -42,8 +42,8 @@ pub async fn read_dhcp_leases_with_names() -> io::Result<Vec<DhcpLeaseLine>> {
}
Ok(leases_with_names)
}
use crate::utils::parse::mac;
use macaddr::MacAddr;
use serde::Serializer;
use std::io::{self, ErrorKind};
use std::net::IpAddr;
@@ -53,15 +53,11 @@ pub struct DhcpLeaseLine {
/// Epoch seconds when the lease expires
pub expires_epoch: u64,
pub ip: IpAddr,
#[serde(serialize_with = "ser_mac")]
#[serde(with = "mac")]
pub mac: MacAddr,
pub name: Option<String>,
}
fn ser_mac<S: Serializer>(m: &MacAddr, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&m.to_string())
}
/// Parse one line of /tmp/dhcp.leases
pub fn parse_dhcp_lease_line(line: &str) -> Option<DhcpLeaseLine> {
let mut c = line.split_whitespace();
+1 -1
View File
@@ -13,7 +13,7 @@ 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) {
let query = match parse_query(q).await {
QueryType::Ip(ip_addr) => DeviceQuery {
filter: Filters {
ips: vec![ip_addr],
+1 -1
View File
@@ -2,6 +2,6 @@ use crate::utils::query::dev;
use axum::Json;
pub async fn devs_router() -> Json<Vec<String>> {
dev::devs_sorted().into()
dev::devs_sorted().await.into()
}
// Device listing endpoints
+1 -1
View File
@@ -13,7 +13,7 @@ pub struct ApiError {
}
impl ApiError {
/// [StatusCode::INTERNAL_SERVER_ERROR]
/// [StatusCode::INTERNAL_SERVER_ERROR] shortcut
pub const fn ise(error: String) -> Self {
Self {
code: StatusCode::INTERNAL_SERVER_ERROR,
+10 -10
View File
@@ -4,11 +4,10 @@ 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::parse::de_many;
use crate::utils::parse::serialize_macs;
use crate::utils::query::get_macs;
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
@@ -31,19 +30,20 @@ pub struct Status<T> {
pub filters: Filters,
}
#[serde_as]
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct Filters {
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
#[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub ips: Vec<IpAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
#[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub devs: Vec<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
#[serde_as(as = "OneOrMany<_>")]
#[serde(default)]
pub nuds: Vec<NUDState>,
#[serde(
default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs"
)]
#[serde_as(as = "OneOrMany<DisplayFromStr>")]
#[serde(default)]
pub macs: Vec<MacAddr>,
}
+2 -2
View File
@@ -4,7 +4,7 @@ use std::net::IpAddr;
/* use crate::arpparse::IpNeighLine;
use crate::route::api::Status; */
use crate::utils::parse::mac::{des_opm, ser_opm};
use crate::utils::parse::mac;
use crate::utils::wake::wake_one;
use axum::{extract::Json, http::StatusCode, response::IntoResponse};
use futures::TryFutureExt;
@@ -44,7 +44,7 @@ pub enum WakeTargetStatus {
pub struct WakeTarget {
#[serde(default)]
pub ip: Option<IpAddr>,
#[serde(default, serialize_with = "ser_opm", deserialize_with = "des_opm")]
#[serde(default, with = "mac::option_mac")]
pub mac: Option<MacAddr>,
}
-10
View File
@@ -1,10 +0,0 @@
use std::io;
pub(crate) async fn exec_command<S: AsRef<std::ffi::OsStr>>(
cmd: S,
args: impl IntoIterator<Item = S>,
) -> io::Result<std::process::Output> {
let mut u = tokio::process::Command::new(cmd);
u.args(args);
u.output().await
}
-56
View File
@@ -1,56 +0,0 @@
use std::{fmt, io};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
/// Failure to resolve a host name to IPs.
DnsResolve { name: String, source: io::Error },
/// External command failed (e.g., ip neigh)
CommandFailed {
cmd: &'static str,
args: Vec<String>,
status: Option<i32>,
stderr: String,
},
/// Generic IO error fallback
Io(io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::DnsResolve { name, source } => {
write!(f, "DNS resolve failed for {name}: {source}")
}
Error::CommandFailed {
cmd,
args,
status,
stderr,
} => {
let code = status
.map(|c| c.to_string())
.unwrap_or_else(|| "signal".into());
write!(f, "{cmd} {args:?} failed (status: {code}): {stderr}",)
}
Error::Io(e) => write!(f, "IO error: {e}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::DnsResolve { source, .. } => Some(source),
Error::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
-2
View File
@@ -9,8 +9,6 @@
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub mod wake;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
+8 -47
View File
@@ -100,52 +100,12 @@ pub fn boolish_str(s: &str) -> bool {
&& t.parse::<u64>().map(|n| n != 0).unwrap_or(false))
}
pub mod de_many {
use serde::Deserialize;
use serde::de;
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
pub fn vec_from_strs<'de, D, T>(des: D) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
let raw: OneOrMany<String> = OneOrMany::<String>::deserialize(des)?;
let mut out = Vec::new();
match raw {
OneOrMany::One(s) => {
let t = s.trim();
if !t.is_empty() {
out.push(t.parse().map_err(de::Error::custom)?);
}
}
OneOrMany::Many(vs) => {
for s in vs {
let t = s.trim();
if t.is_empty() {
continue;
}
out.push(t.parse().map_err(de::Error::custom)?);
}
}
}
Ok(out)
}
}
pub mod mac {
use macaddr::MacAddr;
use serde::{self, Deserialize, Deserializer, de::Error as DeError};
use serde::{Serialize, Serializer, de};
pub fn serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
pub fn _serialize_macs<S>(macs: &[MacAddr], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
@@ -154,7 +114,7 @@ pub mod mac {
}
/// Serialize a MacAddr as a string
pub fn _serialize_mac<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
pub fn serialize<S>(mac: &MacAddr, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
@@ -162,7 +122,7 @@ pub mod mac {
}
/// Deserialize a MacAddr from a string
pub fn _deserialize_mac<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
pub fn _deserialize<'de, D>(deserializer: D) -> Result<MacAddr, D::Error>
where
D: Deserializer<'de>,
{
@@ -170,13 +130,15 @@ pub mod mac {
s.parse::<MacAddr>().map_err(DeError::custom)
}
pub mod option_mac {
use super::*;
/// serialize an [`Option<MacAddr>`]
pub fn ser_opm<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
pub fn serialize<S: Serializer>(bro: &Option<MacAddr>, ser: S) -> Result<S::Ok, S::Error> {
Option::<String>::serialize(&bro.as_ref().map(ToString::to_string), ser)
}
/// deserialize an [`Option<MacAddr>`]
pub fn des_opm<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
pub fn deserialize<'de, D>(des: D) -> Result<Option<MacAddr>, D::Error>
where
D: serde::Deserializer<'de>,
{
@@ -186,5 +148,4 @@ pub mod mac {
.map_err(de::Error::custom)
}
}
pub use mac::*;
}
+11 -32
View File
@@ -1,45 +1,24 @@
use std::{collections::HashSet, fs};
use lda_ipjs::subcommands::address::json as ipjs_json;
use std::collections::HashSet;
pub fn get_dev() -> HashSet<String> {
pub async fn get_dev() -> HashSet<String> {
let mut devs: HashSet<String> = HashSet::new();
if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
for e in rd.flatten() {
if e.file_type()
.map(|ft| {
if ft.is_symlink() {
// true
fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false)
} else {
ft.is_dir()
}
})
.unwrap_or(false)
&& let Ok(name) = e.file_name().into_string()
&& name != "lo"
&& !name.is_empty()
{
devs.insert(name);
}
}
} else if let Ok(txt) = std::fs::read_to_string("/proc/net/dev") {
for line in txt.lines().skip(2) {
if let Some((name, _rest)) = line.split_once(':') {
let n = name.trim().to_string();
if n != "lo" && !n.is_empty() {
devs.insert(n);
}
if let Ok(items) = ipjs_json::get(None).await {
for item in items {
if item.ifname != "lo" && !item.ifname.is_empty() {
devs.insert(item.ifname);
}
}
}
devs
}
pub fn devs_sorted() -> Vec<String> {
let mut v: Vec<String> = get_dev().into_iter().collect();
pub async fn devs_sorted() -> Vec<String> {
let mut v: Vec<String> = get_dev().await.into_iter().collect();
v.sort();
v
}
pub fn has_dev(name: &str) -> bool {
get_dev().contains(name)
pub async fn has_dev(name: &str) -> bool {
get_dev().await.contains(name)
}
+28 -50
View File
@@ -1,28 +1,18 @@
use macaddr::MacAddr;
use crate::arpparse::{self, IpNeighLine, NUDState};
use crate::utils::{
cmd::exec_command,
error::{self, Result},
};
use crate::arpparse::{IpNeighLine, NUDState};
use anyhow::{Context, Result};
use lda_ipjs::subcommands::neighbor as ipjs_neigh;
use std::collections::HashSet;
use std::net::IpAddr;
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
Ok(tokio::net::lookup_host((machine_name, 0))
.await
.map_err(|e| error::Error::DnsResolve {
name: machine_name.to_string(),
source: e,
})?
.with_context(|| format!("DNS resolve failed for {machine_name}"))?
.map(|c| c.ip()))
}
// #[deprecated(
// since = "0.1.5",
// note = "just call once everything with get mac
// and then filter it bro WHY DO YOU EVEN DO TS"
// )]
// good now
//
// Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field.
@@ -45,11 +35,8 @@ pub async fn get_macs(
macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> {
let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect();
let ip_m: HashSet<IpAddr> = futures::future::try_join_all(
machine_names
.iter()
.map(|c| get_ips(c.as_ref())),
)
let ip_m: HashSet<IpAddr> =
futures::future::try_join_all(machine_names.iter().map(|c| get_ips(c.as_ref())))
.await?
.into_iter()
.flatten()
@@ -114,37 +101,28 @@ pub async fn get_mac(
dev: Option<&str>,
state: &[NUDState],
) -> Result<Vec<IpNeighLine>> {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = ip {
args.push("to".into());
args.push(ip.to_string());
}
if let Some(d) = dev {
args.push("dev".into());
args.push(d.to_string());
}
for nud in state {
args.push("nud".into());
args.push(nud.as_ip_neigh_arg().into());
}
let cmd = "ip";
let out = exec_command(cmd, args.iter().map(String::as_str).collect::<Vec<_>>()).await?;
if !out.status.success() {
return Err(error::Error::CommandFailed {
cmd,
args,
status: out.status.code(),
stderr: String::from_utf8_lossy(&out.stderr).into(),
});
}
let lines = String::from_utf8_lossy(&out.stdout);
let parsed = lines.lines().flat_map(arpparse::parse_ip_neigh_line);
let rows: Vec<IpNeighLine> = if let Some(d) = dev {
parsed.map(IpNeighLine::with_dev(d)).collect()
} else {
parsed.collect()
};
Ok(rows)
let ipjs_states: Vec<ipjs_neigh::NUDState> = state.iter().copied().map(Into::into).collect();
let items = ipjs_neigh::json::get(ip, dev, &ipjs_states)
.await
.context("Calling ip -j neigh failed")?;
let lines = items
.into_iter()
.map(|item| IpNeighLine {
ip: item.ip,
dev: Some(item.dev),
mac: item.mac,
state: item
.state
.first()
.copied()
.map(Into::into)
.unwrap_or(NUDState::None),
})
.collect();
Ok(lines)
}
/*
+2 -2
View File
@@ -12,7 +12,7 @@ pub enum QueryType {
Unknown(String),
}
pub fn parse_query(q: String) -> QueryType {
pub async fn parse_query(q: String) -> QueryType {
let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q)
} else {
@@ -36,7 +36,7 @@ pub fn parse_query(q: String) -> QueryType {
return QueryType::Nud(state);
}
// 4) Known device? prefer dev first
if has_dev(s) {
if has_dev(s).await {
return QueryType::Dev(s.to_string());
}
// Default: name last // it will fail also
+2
View File
@@ -1,3 +1,5 @@
//! why did my Head Ass split these into two.
pub mod impls;
use std::{io, net::IpAddr};