3 Commits
25 changed files with 801 additions and 233 deletions

No files matched your search

Generated
+115
View File
@@ -26,6 +26,56 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.100" version = "1.0.100"
@@ -206,6 +256,46 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "clap"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]] [[package]]
name = "color-eyre" name = "color-eyre"
version = "0.6.5" version = "0.6.5"
@@ -233,6 +323,12 @@ dependencies = [
"tracing-error", "tracing-error",
] ]
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -603,6 +699,12 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.17" version = "1.0.17"
@@ -800,6 +902,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "owo-colors" name = "owo-colors"
version = "4.2.3" version = "4.2.3"
@@ -1369,6 +1477,12 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
@@ -1382,6 +1496,7 @@ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
"axum-extra", "axum-extra",
"clap",
"color-eyre", "color-eyre",
"futures", "futures",
"lda-ipjs", "lda-ipjs",
+1
View File
@@ -8,6 +8,7 @@ publish = ["gitea"]
anyhow = "1" anyhow = "1"
axum = { version = "0", features = ["macros"] } axum = { version = "0", features = ["macros"] }
axum-extra = { version = "0", features = ["query"] } axum-extra = { version = "0", features = ["query"] }
clap = { version = "4", features = ["derive"] }
color-eyre = "0" color-eyre = "0"
futures = "0" futures = "0"
macaddr = { version = "1", features = ["serde", "serde_std"] } macaddr = { version = "1", features = ["serde", "serde_std"] }
+14 -2
View File
@@ -5,6 +5,10 @@ version = "0.0.2"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
[features]
default = []
experimental-nl = ["dep:rtnetlink"]
[dependencies] [dependencies]
macaddr = { version = "1", features = ["serde", "serde_std"] } macaddr = { version = "1", features = ["serde", "serde_std"] }
strum = { version = "0", features = ["derive", "strum_macros"] } strum = { version = "0", features = ["derive", "strum_macros"] }
@@ -13,7 +17,15 @@ serde = { version = "1", features = ["derive"] }
serde_with = { version = "3", features = ["json"] } serde_with = { version = "3", features = ["json"] }
thiserror = "2" thiserror = "2"
anyhow = "1" anyhow = "1"
tokio = { version = "1", features = ["fs", "process", "rt-multi-thread", "io-util", "macros"] } tokio = { version = "1", features = [
rtnetlink = "0" "fs",
"process",
"rt-multi-thread",
"io-util",
"macros",
] }
futures = "0" futures = "0"
[target.'cfg(unix)'.dependencies]
rtnetlink = { version = "0", optional = true }
-1
View File
@@ -13,7 +13,6 @@
//! i also need to see devices and idk MAYBE maybe not MAYBE UHHHHHH maybe broadcast //! 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 //! 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 subcommands;
pub mod utils; pub mod utils;
+17
View File
@@ -5,8 +5,10 @@
//! lowk why its free but its indirection and its ass //! lowk why its free but its indirection and its ass
pub mod json; pub mod json;
#[cfg(all(unix, feature = "experimental-nl"))]
pub mod nl; pub mod nl;
pub use crate::subcommands::Backend;
use crate::utils::serialize::mac::option_mac; use crate::utils::serialize::mac::option_mac;
use macaddr::MacAddr; use macaddr::MacAddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -40,3 +42,18 @@ pub struct AddrInfo {
pub label: Option<String>, pub label: Option<String>,
// many more exist; we only take what we need // many more exist; we only take what we need
} }
pub async fn get(dev: Option<&str>) -> anyhow::Result<Vec<AddrOutput>> {
get_with_backend(Backend::Json, dev).await
}
pub async fn get_with_backend(
backend: Backend,
dev: Option<&str>,
) -> anyhow::Result<Vec<AddrOutput>> {
match backend {
Backend::Json => json::get(dev).await,
#[cfg(all(unix, feature = "experimental-nl"))]
Backend::Netlink => nl::get(dev).await,
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
//! i said i aint doing ts no more why am i still here //! i said i aint doing ts no more why am i still here
#![cfg(unix)]
use futures::TryStreamExt; use futures::TryStreamExt;
use crate::subcommands::address::AddrOutput; use crate::subcommands::address::AddrOutput;
+7
View File
@@ -1,2 +1,9 @@
pub mod address; pub mod address;
pub mod neighbor; pub mod neighbor;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backend {
Json,
#[cfg(all(unix, feature = "experimental-nl"))]
Netlink,
}
+27
View File
@@ -5,8 +5,10 @@
//! yes. this is a real call. //! yes. this is a real call.
pub mod json; pub mod json;
#[cfg(all(unix, feature = "experimental-nl"))]
pub mod nl; pub mod nl;
pub use crate::subcommands::Backend;
use crate::utils::serialize::mac::option_mac; use crate::utils::serialize::mac::option_mac;
use std::net::IpAddr; use std::net::IpAddr;
@@ -80,3 +82,28 @@ pub struct NeighborItem {
#[serde(default)] #[serde(default)]
pub state: Vec<NUDState>, pub state: Vec<NUDState>,
} }
pub async fn get(
ip: Option<IpAddr>,
dev: Option<&str>,
nud: &[NUDState],
) -> anyhow::Result<Vec<NeighborItem>> {
get_with_backend(Backend::Json, ip, dev, nud).await
}
pub async fn get_with_backend(
backend: Backend,
ip: Option<IpAddr>,
dev: Option<&str>,
nud: &[NUDState],
) -> anyhow::Result<Vec<NeighborItem>> {
match backend {
Backend::Json => json::get(ip, dev, nud).await,
#[cfg(all(unix, feature = "experimental-nl"))]
Backend::Netlink => {
let ips: Vec<IpAddr> = ip.into_iter().collect();
let devs: Vec<&str> = dev.into_iter().collect();
nl::get(&ips, &devs, nud, &[]).await
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
//! rtnetlink-based neighbor table query. One syscall, filter in userspace. //! rtnetlink-based neighbor table query. One syscall, filter in userspace.
#![cfg(unix)]
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
net::IpAddr, net::IpAddr,
+26 -21
View File
@@ -2,16 +2,17 @@ use std::collections::HashSet;
use lda_ipjs::subcommands::{address, neighbor}; use lda_ipjs::subcommands::{address, neighbor};
#[cfg(all(unix, feature = "experimental-nl"))]
#[tokio::test] // ← Use tokio::test instead of manual #[tokio::main] #[tokio::test] // ← Use tokio::test instead of manual #[tokio::main]
async fn ball1() -> anyhow::Result<()> { async fn ball1() -> anyhow::Result<()> {
let result = neighbor::nl::get(&[], &[] as &[&str], &[], &[]).await?; let result = neighbor::get_with_backend(neighbor::Backend::Netlink, None, None, &[]).await?;
println!("netlink results: {:?}", result); println!("netlink results: {:?}", result);
Ok(()) // ← Don't force error, let it succeed Ok(()) // ← Don't force error, let it succeed
} }
#[tokio::test] #[tokio::test]
async fn ball2() -> anyhow::Result<()> { async fn ball2() -> anyhow::Result<()> {
let result = neighbor::json::get(None, None, &[]).await?; let result = neighbor::get_with_backend(neighbor::Backend::Json, None, None, &[]).await?;
println!("json results: {:?}", result); println!("json results: {:?}", result);
Ok(()) Ok(())
} }
@@ -91,30 +92,34 @@ impl TypeName for serde_json::Value {
#[tokio::test] #[tokio::test]
async fn ball_compare_backends() -> anyhow::Result<()> { async fn ball_compare_backends() -> anyhow::Result<()> {
println!("=== JSON Backend ==="); println!("=== JSON Backend ===");
let json_result = neighbor::json::get(None, None, &[]).await?; let json_result = neighbor::get_with_backend(neighbor::Backend::Json, None, None, &[]).await?;
println!("Got {} entries from JSON", json_result.len()); println!("Got {} entries from JSON", json_result.len());
println!("\n=== Netlink Backend ==="); #[cfg(all(unix, feature = "experimental-nl"))]
let nl_result = neighbor::nl::get(&[], &[] as &[&str], &[], &[]).await?; {
println!("Got {} entries from netlink", nl_result.len()); println!("\n=== Netlink Backend ===");
let nl_result =
neighbor::get_with_backend(neighbor::Backend::Netlink, None, None, &[]).await?;
println!("Got {} entries from netlink", nl_result.len());
// Compare counts // Compare counts
if json_result.len() != nl_result.len() { 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!( println!(
"\n⚠️ Count mismatch! JSON: {}, Netlink: {}", "istg {len1} == {len2} or else",
json_result.len(), len1 = a.len(),
nl_result.len() len2 = b.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(()) Ok(())
} }
@@ -126,7 +131,7 @@ async fn ball_compare_backends() -> anyhow::Result<()> {
#[tokio::test] #[tokio::test]
async fn ipjas() -> anyhow::Result<()> { async fn ipjas() -> anyhow::Result<()> {
let cuh = address::json::get(None).await?; let cuh = address::get_with_backend(address::Backend::Json, None).await?;
println!("{cuh:#?}"); println!("{cuh:#?}");
Ok(()) Ok(())
} }
+2
View File
@@ -17,6 +17,8 @@ try { $PSStyle.OutputRendering = 'Host' } catch {}
. "$PSScriptRoot/lib.ps1" . "$PSScriptRoot/lib.ps1"
$Pass = Get-DefaultPassword $Pass
function Get-DeployScript { function Get-DeployScript {
param($DeployPreferred, $DeployTmp, $RemoteTmp, $RemotePath, $RestartFlag) param($DeployPreferred, $DeployTmp, $RemoteTmp, $RemotePath, $RestartFlag)
return @" return @"
+1 -1
View File
@@ -9,7 +9,7 @@ BIN=/root/.bin/wakey
start_service() { start_service() {
procd_open_instance procd_open_instance
procd_set_param command "$BIN" procd_set_param command "$BIN" http --host :: --port 12012
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
+15
View File
@@ -1,5 +1,20 @@
# Shared functions for wakey scripts # Shared functions for wakey scripts
function Get-DefaultPassword {
param([string]$Password)
if ($Password) {
return $Password
}
$pwPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'ar_data/pw'
if (-not (Test-Path $pwPath)) {
throw "Password not provided and default file not found: $pwPath"
}
return (Get-Content -Raw $pwPath).Trim()
}
function Invoke-Ext { function Invoke-Ext {
param($Exe, $Arguments, $Label) param($Exe, $Arguments, $Label)
$displayArgs = $Arguments.Clone() $displayArgs = $Arguments.Clone()
+2
View File
@@ -16,6 +16,8 @@ $ErrorActionPreference = "Stop"
. "$PSScriptRoot/lib.ps1" . "$PSScriptRoot/lib.ps1"
$password = Get-DefaultPassword $password
# Build tests and capture output # Build tests and capture output
Write-Host "Building tests for $Package..." -ForegroundColor Cyan Write-Host "Building tests for $Package..." -ForegroundColor Cyan
+145
View File
@@ -0,0 +1,145 @@
use serde::Serialize;
use wakey_core::parse::mac;
use wakey_core::{
DeviceFilters, DhcpLeaseWithState, NeighborEntry, Status, WakeResult, WakeTargetResult,
};
#[derive(Debug, Clone, Serialize)]
pub struct LegacyStatusRow {
pub ip: std::net::IpAddr,
pub dev: Option<String>,
#[serde(with = "mac::option_mac")]
pub mac: Option<macaddr::MacAddr>,
pub state: wakey_core::NeighborState,
}
#[derive(Debug, Clone, Serialize)]
pub struct LegacyStatusResponse {
pub name: Option<String>,
pub table: Vec<LegacyStatusRow>,
pub filters: DeviceFilters,
}
#[derive(Debug, Clone, Serialize)]
pub struct LegacyLeaseRow {
pub expires_epoch: u64,
pub ip: std::net::IpAddr,
#[serde(with = "mac")]
pub mac: macaddr::MacAddr,
pub name: Option<String>,
pub nud_state: Option<wakey_core::NeighborState>,
}
#[derive(Debug, Clone, Serialize)]
pub struct LegacyWakeResult {
pub result: Vec<LegacyWakeResultRow>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct LegacyWakeResultRow {
#[serde(flatten)]
pub target: wakey_core::WakeTarget,
pub status: wakey_core::WakeStatus,
}
pub fn legacy_status_from_domain(status: Status<NeighborEntry>) -> LegacyStatusResponse {
LegacyStatusResponse {
name: status.name,
table: status.table.into_iter().map(legacy_status_row).collect(),
filters: status.filters,
}
}
pub fn legacy_status_row(row: NeighborEntry) -> LegacyStatusRow {
LegacyStatusRow {
ip: row.ip,
dev: row.dev,
mac: row.mac,
state: row.state,
}
}
pub fn legacy_leases_from_domain(leases: Vec<DhcpLeaseWithState>) -> Vec<LegacyLeaseRow> {
leases.into_iter().map(legacy_lease_row).collect()
}
pub fn legacy_lease_row(lease: DhcpLeaseWithState) -> LegacyLeaseRow {
LegacyLeaseRow {
expires_epoch: lease.lease_line.expires_epoch,
ip: lease.lease_line.ip,
mac: lease.lease_line.mac,
name: lease.lease_line.name,
nud_state: lease.nud_state,
}
}
pub fn legacy_wake_from_domain(result: WakeResult) -> LegacyWakeResult {
LegacyWakeResult {
result: result.result.into_iter().map(legacy_wake_row).collect(),
}
}
pub fn legacy_wake_row(row: WakeTargetResult) -> LegacyWakeResultRow {
LegacyWakeResultRow {
target: row.target,
status: row.status,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
use wakey_core::{DhcpLease, NeighborState, WakeStatus, WakeTarget};
#[test]
fn maps_status_to_legacy_shape() {
let status = Status {
name: Some("pc".into()),
table: vec![NeighborEntry {
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
dev: Some("br-lan".into()),
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
state: NeighborState::Reachable,
}],
filters: DeviceFilters::default(),
};
let legacy = legacy_status_from_domain(status);
assert_eq!(legacy.name.as_deref(), Some("pc"));
assert_eq!(legacy.table.len(), 1);
assert_eq!(legacy.table[0].state, NeighborState::Reachable);
}
#[test]
fn maps_leases_to_legacy_shape() {
let leases = vec![DhcpLeaseWithState {
lease_line: DhcpLease {
expires_epoch: 42,
ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
mac: "aa:bb:cc:dd:ee:ff".parse().expect("mac"),
name: Some("pc".into()),
},
nud_state: Some(NeighborState::Reachable),
}];
let legacy = legacy_leases_from_domain(leases);
assert_eq!(legacy.len(), 1);
assert_eq!(legacy[0].name.as_deref(), Some("pc"));
assert_eq!(legacy[0].nud_state, Some(NeighborState::Reachable));
}
#[test]
fn maps_wake_to_legacy_shape() {
let result = WakeResult {
result: vec![WakeTargetResult {
target: WakeTarget {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
},
status: WakeStatus::Succeed,
}],
};
let legacy = legacy_wake_from_domain(result);
assert_eq!(legacy.result.len(), 1);
assert_eq!(legacy.result[0].status, WakeStatus::Succeed);
}
}
+209
View File
@@ -0,0 +1,209 @@
pub mod arpparse;
pub mod compat;
pub mod dhcpparse;
pub mod route;
pub mod utils;
use std::{io, net::SocketAddr};
use anyhow::{Context, Result};
use axum::Router;
use tokio::net::TcpListener;
use tower_http::services::ServeDir;
use wakey_core::{
DeviceFilters, DeviceQuery, DhcpLease, DhcpLeaseWithState, NeighborEntry, QueryInput, Status,
WakeResult, WakeTarget,
};
pub type StatusResponse = Status<NeighborEntry>;
pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> {
Ok(
match wakey_linux::devices::classify_query(input.into()).await {
QueryInput::Ip(ip_addr) => DeviceQuery {
filter: DeviceFilters {
ips: vec![ip_addr],
..Default::default()
},
..Default::default()
},
QueryInput::Mac(mac_addr) => DeviceQuery {
filter: DeviceFilters {
macs: vec![mac_addr],
..Default::default()
},
..Default::default()
},
QueryInput::Dev(dev) => DeviceQuery {
filter: DeviceFilters {
devs: vec![dev],
..Default::default()
},
..Default::default()
},
QueryInput::Nud(state) => DeviceQuery {
filter: DeviceFilters {
nuds: vec![state],
..Default::default()
},
..Default::default()
},
QueryInput::Name(name) => DeviceQuery {
name: Some(name),
..Default::default()
},
},
)
}
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
let table = wakey_linux::devices::query_status(&query).await?;
Ok(Status {
name: query.name,
table,
filters: query.filter,
})
}
pub async fn get_status_for_input(input: impl Into<String>) -> Result<StatusResponse> {
let query = resolve_query(input).await?;
get_status(query).await
}
pub async fn get_leases(include_state: bool) -> Result<Vec<DhcpLeaseWithState>> {
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
.await
.context("failed to read DHCP leases")?;
if include_state {
Ok(wakey_linux::dhcp::enrich_leases_with_nud_state(leases).await)
} else {
Ok(leases
.into_iter()
.map(|lease_line| DhcpLeaseWithState {
lease_line,
nud_state: None,
})
.collect())
}
}
pub async fn wake_targets(targets: Vec<WakeTarget>) -> Result<WakeResult> {
let result = wakey_linux::wake::wake_many(targets)
.await
.context("failed to send wake packets")?;
Ok(WakeResult { result })
}
pub async fn wake_from_query(input: impl Into<String>) -> Result<WakeResult> {
let status = get_status_for_input(input).await?;
let targets = status
.table
.into_iter()
.map(|entry| WakeTarget {
ip: Some(entry.ip),
mac: entry.mac,
})
.collect();
wake_targets(targets).await
}
pub async fn list_interfaces() -> Result<Vec<String>> {
Ok(wakey_linux::devices::devs_sorted().await)
}
pub async fn get_ips(name: impl AsRef<str>) -> Result<Vec<std::net::IpAddr>> {
Ok(wakey_linux::devices::get_ips(name.as_ref())
.await?
.collect())
}
pub fn http_app(static_root: std::path::PathBuf) -> Router {
Router::new()
.nest("/api", route::api_router())
.fallback_service(axum::routing::get_service(
ServeDir::new(static_root)
.append_index_html_on_directories(true)
.precompressed_br()
.precompressed_deflate()
.precompressed_gzip()
.precompressed_zstd(),
))
}
pub async fn serve_http(addr: SocketAddr, static_root: std::path::PathBuf) -> io::Result<()> {
let listener = TcpListener::bind(addr).await?;
axum::serve(listener, http_app(static_root).into_make_service()).await
}
pub async fn serve_http_from_current_exe(addr: SocketAddr) -> io::Result<()> {
let exe = std::env::current_exe()?;
let root = exe
.parent()
.ok_or_else(|| io::Error::other("no parent dir"))?;
serve_http(addr, root.join("static")).await
}
pub fn leases_without_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> {
leases
.into_iter()
.map(|lease_line| DhcpLeaseWithState {
lease_line,
nud_state: None,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{IpAddr, Ipv4Addr};
use wakey_core::{NeighborState, WakeStatus};
#[tokio::test]
async fn resolve_query_parses_ip() {
let query = resolve_query("192.168.1.10").await.expect("resolve query");
assert_eq!(
query.filter.ips,
vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10))]
);
}
#[tokio::test]
async fn resolve_query_parses_mac() {
let query = resolve_query("aa:bb:cc:dd:ee:ff")
.await
.expect("resolve query");
assert_eq!(query.filter.macs.len(), 1);
}
#[tokio::test]
async fn resolve_query_parses_nud() {
let query = resolve_query("reachable").await.expect("resolve query");
assert_eq!(query.filter.nuds, vec![NeighborState::Reachable]);
}
#[test]
fn leases_without_state_clears_nud_state() {
let leases = vec![DhcpLease {
expires_epoch: 1,
ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
mac: "aa:bb:cc:dd:ee:ff".parse().expect("mac"),
name: Some("pc".into()),
}];
let out = leases_without_state(leases);
assert_eq!(out.len(), 1);
assert!(out[0].nud_state.is_none());
}
#[tokio::test]
async fn wake_targets_marks_incomplete() {
let out = wake_targets(vec![WakeTarget {
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
mac: None,
}])
.await
.expect("wake");
assert_eq!(out.result.len(), 1);
assert_eq!(out.result[0].status, WakeStatus::Incomplete);
}
}
+114 -53
View File
@@ -1,65 +1,126 @@
//! braindead version v0.1.x use std::net::{IpAddr, SocketAddr};
//!
//! # whats next
//!
//! for version 2 i hope to have:
//!
//! 1. idk reworked frontend;
//! 2. incorporate ip -j;
//! 3. small 1-5 second caching;
use axum::Router; use clap::{Args, Parser, Subcommand};
use tokio::net::TcpListener; use wakey_core::{DeviceFilters, DeviceQuery};
mod arpparse;
mod dhcpparse;
mod route;
mod utils;
use std::{env, io};
#[cfg(target_os = "linux")] #[derive(Parser)]
#[tokio::main] #[command(name = "wakey")]
async fn entry() -> io::Result<()> { #[command(version, about = "Wakey service CLI and HTTP adapter")]
use crate::route::api_router; struct Cli {
use axum::routing::get_service; #[command(subcommand)]
use tower_http::services::ServeDir; command: Command,
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()
// .route("/home", get(home))
// .route("/", get(home_2))
// .merge(home_2_route())
// .route("/status", get(get_status_2))
.nest("/api", api_router())
.fallback_service(get_service(static_dir));
let port = TcpListener::bind("[::]:12012").await?; #[derive(Subcommand)]
axum::serve(port, app.into_make_service()).await?; enum Command {
Ok(()) Http(HttpArgs),
Status(StatusArgs),
Leases(LeasesArgs),
Wake(WakeArgs),
Devs,
}
#[derive(Args)]
struct HttpArgs {
#[arg(long, default_value = "::")]
host: IpAddr,
#[arg(long, default_value_t = 12012)]
port: u16,
}
#[derive(Args)]
struct LeasesArgs {
#[arg(long)]
include_state: bool,
}
#[derive(Args)]
struct WakeArgs {
query: String,
}
#[derive(Args)]
struct StatusArgs {
query: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long = "ip")]
ips: Vec<std::net::IpAddr>,
#[arg(long = "dev")]
devs: Vec<String>,
#[arg(long = "nud")]
nuds: Vec<wakey_core::NeighborState>,
#[arg(long = "mac")]
macs: Vec<macaddr::MacAddr>,
}
fn status_args_to_query(args: StatusArgs) -> wakey_core::DeviceQuery {
if let Some(query) = args.query.as_ref()
&& args.name.is_none()
&& args.ips.is_empty()
&& args.devs.is_empty()
&& args.nuds.is_empty()
&& args.macs.is_empty()
{
return DeviceQuery {
name: Some(query.clone()),
..Default::default()
};
}
DeviceQuery {
name: args.name.or(args.query),
filter: DeviceFilters {
ips: args.ips,
devs: args.devs,
nuds: args.nuds,
macs: args.macs,
},
}
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn main() -> color_eyre::Result<()> { fn main() -> anyhow::Result<()> {
use std::net::ToSocketAddrs; Err(anyhow::anyhow!(
color_eyre::install()?;
// use crate::arpparse::NUDState;
// println!("{}", NUDState::Reachable.to_string().to_lowercase());
println!("{:?}", "svuhuvshdv:331".to_socket_addrs());
// Err(Os { code: 11001, kind: Uncategorized, message: "No such host is known." })
Err(color_eyre::eyre::eyre!(
"OS not supported! run this on your ahh router!" "OS not supported! run this on your ahh router!"
)) ))
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn main() -> color_eyre::Result<()> { #[tokio::main]
color_eyre::install()?; async fn main() -> anyhow::Result<()> {
Ok(entry()?) let cli = Cli::parse();
match cli.command {
Command::Http(args) => {
let addr = SocketAddr::new(args.host, args.port);
wakey::serve_http_from_current_exe(addr).await?;
}
Command::Status(args) => {
let query = status_args_to_query(args);
let status = if query.name.is_some()
&& query.filter.ips.is_empty()
&& query.filter.devs.is_empty()
&& query.filter.nuds.is_empty()
&& query.filter.macs.is_empty()
{
wakey::get_status_for_input(query.name.clone().unwrap_or_default()).await?
} else {
wakey::get_status(query).await?
};
println!("{}", serde_json::to_string_pretty(&status)?);
}
Command::Leases(args) => {
let leases = wakey::get_leases(args.include_state).await?;
println!("{}", serde_json::to_string_pretty(&leases)?);
}
Command::Wake(args) => {
let result = wakey::wake_from_query(args.query).await?;
println!("{}", serde_json::to_string_pretty(&result)?);
}
Command::Devs => {
let devs = wakey::list_interfaces().await?;
println!("{}", serde_json::to_string_pretty(&devs)?);
}
}
Ok(())
} }
+11 -39
View File
@@ -1,52 +1,24 @@
use crate::route::error::ApiError; use crate::route::error::ApiError;
use crate::utils::query::parser::{QueryType, parse_query};
use axum::Json; use axum::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::{extract::Path, response::Redirect}; use axum::{extract::Path, response::Redirect};
use wakey_core::{DeviceFilters as Filters, DeviceQuery}; use wakey_core::DeviceQuery;
use crate::route::status::NamePath; use crate::route::status::NamePath;
use crate::utils::query::get_ips;
// Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly // Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly
pub async fn status_smart_redirect( pub async fn status_smart_redirect(
Path(q): Path<String>, Path(q): Path<String>,
) -> axum::response::Result<Redirect, impl IntoResponse> { ) -> axum::response::Result<Redirect, impl IntoResponse> {
// no less bullshit let query: DeviceQuery = match crate::resolve_query(q).await {
let query = match parse_query(q).await { Ok(query) => query,
QueryType::Ip(ip_addr) => DeviceQuery { Err(e) => {
filter: Filters { return Err(ApiError {
ips: vec![ip_addr], error: e.to_string(),
..Default::default() code: StatusCode::BAD_GATEWAY,
}, });
..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::Name(n) => DeviceQuery {
name: Some(n),
..Default::default()
},
}; };
match serde_html_form::to_string(query) { match serde_html_form::to_string(query) {
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))), Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
@@ -65,8 +37,8 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
} }
pub async fn ip(Path(name): Path<String>) -> impl IntoResponse { pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
get_ips(&name).await.map_or_else( crate::get_ips(&name).await.map_or_else(
|e| ApiError::ise(e.to_string()).into_response(), |e| ApiError::ise(e.to_string()).into_response(),
|ips| Json(ips.collect::<Vec<_>>()).into_response(), |ips| Json(ips).into_response(),
) )
} }
+7 -4
View File
@@ -1,7 +1,10 @@
use crate::utils::query::dev; use crate::route::error::ApiError;
use axum::Json; use axum::{Json, response::IntoResponse};
pub async fn devs_router() -> Json<Vec<String>> { pub async fn devs_router() -> impl IntoResponse {
dev::devs_sorted().await.into() match crate::list_interfaces().await {
Ok(devs) => Json(devs).into_response(),
Err(e) => ApiError::ise(e.to_string()).into_response(),
}
} }
// Device listing endpoints // Device listing endpoints
+11 -19
View File
@@ -1,8 +1,4 @@
use crate::{ use crate::{route::error::ApiError, utils::parse::boolish_str};
dhcpparse::read_dhcp_leases_with_names,
route::error::ApiError,
utils::{parse::boolish_str, query::enrich_leases_with_nud_state},
};
use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse}; use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
// DHCP lease endpoints // DHCP lease endpoints
@@ -11,21 +7,17 @@ pub struct DhcpLeasesQueryRaw {
include_state: Option<String>, include_state: Option<String>,
} }
pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl IntoResponse { pub async fn get_dhcp_leases(
let include_state = raw Query(DhcpLeasesQueryRaw { include_state }): Query<DhcpLeasesQueryRaw>,
.include_state ) -> impl IntoResponse {
.as_deref() let include_state = include_state.as_deref().map(boolish_str).unwrap_or(false);
.map(boolish_str)
.unwrap_or(false);
match read_dhcp_leases_with_names().await { match crate::get_leases(include_state).await {
Ok(leases_with_names) => { Ok(leases) => (
if !include_state { StatusCode::OK,
return (StatusCode::OK, Json(leases_with_names)).into_response(); Json(crate::compat::legacy_leases_from_domain(leases)),
} )
let out = enrich_leases_with_nud_state(leases_with_names).await; .into_response(),
(StatusCode::OK, Json(out)).into_response()
}
Err(e) => ApiError { Err(e) => ApiError {
error: e.to_string(), error: e.to_string(),
code: StatusCode::BAD_GATEWAY, code: StatusCode::BAD_GATEWAY,
+5 -24
View File
@@ -1,32 +1,13 @@
use crate::route::error::ApiError; use crate::route::error::ApiError;
use crate::utils::query::get_macs;
use axum::{Json, http::StatusCode, response::IntoResponse}; use axum::{Json, http::StatusCode, response::IntoResponse};
use axum_extra::extract::Query; use axum_extra::extract::Query;
pub use wakey_core::{DeviceQuery, NamePath, Status}; pub use wakey_core::{DeviceQuery, NamePath};
pub async fn get_status_json( pub async fn get_status_json(Query(query): Query<DeviceQuery>) -> impl IntoResponse {
Query(DeviceQuery { match crate::get_status(query).await {
name, Ok(status) => (
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, StatusCode::OK,
Json(Status { Json(crate::compat::legacy_status_from_domain(status)),
name,
table,
filters,
}),
) )
.into_response(), .into_response(),
Err(error) => ApiError { Err(error) => ApiError {
+17
View File
@@ -0,0 +1,17 @@
use crate::route::error::ApiError;
use axum::{extract::Json, http::StatusCode, response::IntoResponse};
pub use wakey_core::WakeTarget;
pub async fn wake_multi(Json(req): Json<Vec<WakeTarget>>) -> impl IntoResponse {
match crate::wake_targets(req).await {
Ok(result) => (
StatusCode::OK,
Json(crate::compat::legacy_wake_from_domain(result)),
)
.into_response(),
Err(error) => {
let error = format!("Error: {}", error);
ApiError::ise(error).into_response()
}
}
}
-60
View File
@@ -1,60 +0,0 @@
//! impls are at [`utils::wake::impl`](crate::utils::wake::r#impl) for some reason
use std::io;
/* use crate::arpparse::IpNeighLine;
use crate::route::api::Status; */
use crate::route::error::ApiError;
use crate::utils::wake::wake_one;
use axum::{extract::Json, http::StatusCode, response::IntoResponse};
use futures::TryFutureExt;
use tokio::net::UdpSocket;
pub use wakey_core::{WakeResult, WakeTarget, WakeTargetResult};
pub async fn wake_multi(Json(req): Json<Vec<WakeTarget>>) -> impl IntoResponse {
match wake_multi_split(req).await {
Ok(result) => (StatusCode::OK, Json(WakeResult { result })).into_response(),
Err(error) => {
let error = format!("Error: {}", error);
ApiError::ise(error).into_response()
}
}
}
/// 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_complete() {
WakeTargetResult::incomplete(c)
} else {
let t = c.try_into().expect("complete struct failed to try_into");
wake_one(&sock, t).await
}
});
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 {
// }
+1
View File
@@ -15,3 +15,4 @@ wakey-core = { path = "../wakey-core" }
path = "../ipjs" path = "../ipjs"
registry = "gitea" registry = "gitea"
version = "*" version = "*"
features = ["experimental-nl"]
+52 -7
View File
@@ -46,14 +46,59 @@ pub async fn get_neighbors(
let nud_filter: Vec<neighbor::NUDState> = state.iter().copied().map(to_ipjs_state).collect(); let nud_filter: Vec<neighbor::NUDState> = state.iter().copied().map(to_ipjs_state).collect();
let dev_strs: Vec<&str> = devs.iter().map(AsRef::as_ref).collect(); let dev_strs: Vec<&str> = devs.iter().map(AsRef::as_ref).collect();
let results = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs) #[cfg(unix)]
.await {
.context("rtnetlink failed")? let results = neighbor::nl::get(&ip_filter, &dev_strs, &nud_filter, macs)
.into_iter() .await
.map(map_neighbor_item) .context("rtnetlink failed")?
.collect(); .into_iter()
.map(map_neighbor_item)
.collect();
Ok(results)
}
Ok(results) #[cfg(not(unix))]
{
let mut results = Vec::new();
if ip_filter.is_empty() {
let dev = dev_strs.first().copied();
results = neighbor::get_with_backend(neighbor::Backend::Json, None, dev, &nud_filter)
.await
.context("ip -j neigh failed")?
.into_iter()
.map(map_neighbor_item)
.collect();
} else {
for ip in &ip_filter {
let dev = dev_strs.first().copied();
let mut rows = neighbor::get_with_backend(
neighbor::Backend::Json,
Some(*ip),
dev,
&nud_filter,
)
.await
.with_context(|| format!("ip -j neigh failed for {ip}"))?
.into_iter()
.map(map_neighbor_item)
.collect::<Vec<_>>();
results.append(&mut rows);
}
}
if !dev_strs.is_empty() {
results.retain(|row| row.dev.as_deref().is_some_and(|d| dev_strs.contains(&d)));
}
if !macs.is_empty() {
let mac_set: HashSet<macaddr::MacAddr> = macs.iter().copied().collect();
results.retain(|row| row.mac.is_some_and(|m| mac_set.contains(&m)));
}
if !ip_filter.is_empty() {
let ip_set: HashSet<IpAddr> = ip_filter.iter().copied().collect();
results.retain(|row| ip_set.contains(&row.ip));
}
Ok(results)
}
} }
pub async fn query_status(query: &DeviceQuery) -> Result<Vec<NeighborEntry>> { pub async fn query_status(query: &DeviceQuery) -> Result<Vec<NeighborEntry>> {