sweep!
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
pub mod compat;
|
||||
pub mod route;
|
||||
|
||||
use std::{io, net::SocketAddr};
|
||||
|
||||
use axum::Router;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::services::ServeDir;
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
use crate::route::error::ApiError;
|
||||
use crate::http::route::error::ApiError;
|
||||
use axum::Json;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{extract::Path, response::Redirect};
|
||||
use wakey_core::DeviceQuery;
|
||||
|
||||
use crate::route::status::NamePath;
|
||||
use crate::http::route::status::NamePath;
|
||||
|
||||
// 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> {
|
||||
let query: DeviceQuery = match crate::resolve_query(q).await {
|
||||
let query: DeviceQuery = match crate::service::resolve_query(q).await {
|
||||
Ok(query) => query,
|
||||
Err(e) => {
|
||||
return Err(ApiError {
|
||||
@@ -37,7 +37,7 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
|
||||
}
|
||||
|
||||
pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
|
||||
crate::get_ips(&name).await.map_or_else(
|
||||
crate::service::get_ips(&name).await.map_or_else(
|
||||
|e| ApiError::ise(e.to_string()).into_response(),
|
||||
|ips| Json(ips).into_response(),
|
||||
)
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::route::error::ApiError;
|
||||
use crate::http::route::error::ApiError;
|
||||
use axum::{Json, response::IntoResponse};
|
||||
|
||||
pub async fn devs_router() -> impl IntoResponse {
|
||||
match crate::list_interfaces().await {
|
||||
match crate::service::list_interfaces().await {
|
||||
Ok(devs) => Json(devs).into_response(),
|
||||
Err(e) => ApiError::ise(e.to_string()).into_response(),
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{route::error::ApiError, utils::parse::boolish_str};
|
||||
use crate::{http::route::error::ApiError, utils::parse::boolish_str};
|
||||
use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
|
||||
|
||||
// DHCP lease endpoints
|
||||
@@ -12,10 +12,10 @@ pub async fn get_dhcp_leases(
|
||||
) -> impl IntoResponse {
|
||||
let include_state = include_state.as_deref().map(boolish_str).unwrap_or(false);
|
||||
|
||||
match crate::get_leases(wakey_core::LeaseQuery { include_state }).await {
|
||||
match crate::service::get_leases(wakey_core::LeaseQuery { include_state }).await {
|
||||
Ok(leases) => (
|
||||
StatusCode::OK,
|
||||
Json(crate::compat::legacy_leases_from_domain(leases)),
|
||||
Json(crate::http::compat::legacy_leases_from_domain(leases)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => ApiError {
|
||||
@@ -5,16 +5,15 @@ 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::error::ApiError;
|
||||
use crate::route::status::get_status_json;
|
||||
use crate::route::wake::wake_multi;
|
||||
use crate::http::route::api::ip;
|
||||
use crate::http::route::api::status_redirect;
|
||||
use crate::http::route::api::status_smart_redirect;
|
||||
use crate::http::route::devs::devs_router;
|
||||
use crate::http::route::dhcp::get_dhcp_leases;
|
||||
use crate::http::route::error::ApiError;
|
||||
use crate::http::route::status::get_status_json;
|
||||
use crate::http::route::wake::wake_multi;
|
||||
use crate::legacy::dhcpparse::load_mac_name_cache;
|
||||
|
||||
use axum::Json;
|
||||
use axum::Router;
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::route::error::ApiError;
|
||||
use crate::http::route::error::ApiError;
|
||||
use axum::{Json, http::StatusCode, response::IntoResponse};
|
||||
use axum_extra::extract::Query;
|
||||
pub use wakey_core::{DeviceQuery, NamePath};
|
||||
|
||||
pub async fn get_status_json(Query(query): Query<DeviceQuery>) -> impl IntoResponse {
|
||||
match crate::inventory(query.clone()).await {
|
||||
match crate::service::inventory(query.clone()).await {
|
||||
Ok(inventory) => (
|
||||
StatusCode::OK,
|
||||
Json(crate::compat::legacy_status_from_inventory(
|
||||
Json(crate::http::compat::legacy_status_from_inventory(
|
||||
inventory,
|
||||
query.name,
|
||||
query.filter,
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::route::error::ApiError;
|
||||
use crate::http::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 {
|
||||
match crate::service::wake_targets(req).await {
|
||||
Ok(result) => (
|
||||
StatusCode::OK,
|
||||
Json(crate::compat::legacy_wake_from_domain(result)),
|
||||
Json(crate::http::compat::legacy_wake_from_domain(result)),
|
||||
)
|
||||
.into_response(),
|
||||
Err(error) => {
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod arpparse;
|
||||
pub mod dhcpparse;
|
||||
+15
-326
@@ -1,335 +1,24 @@
|
||||
pub mod arpparse;
|
||||
pub mod compat;
|
||||
pub mod dhcpparse;
|
||||
pub mod route;
|
||||
pub mod http;
|
||||
pub mod legacy;
|
||||
pub mod service;
|
||||
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::{
|
||||
Device, DeviceFilters, DeviceInventory, DeviceQuery, DhcpLease, DhcpLeaseWithState,
|
||||
InterfaceSummary, LeaseQuery, NeighborEntry, Presence, Query, QueryInput, Status, WakeResult,
|
||||
WakeTarget,
|
||||
pub use http::{http_app, serve_http, serve_http_from_current_exe};
|
||||
pub use service::{
|
||||
StatusResponse, broadcast_wake_targets, device_to_status_rows, get_interface_summaries,
|
||||
get_interface_summary, get_ips, get_leases, get_status, get_status_for_input, inventory,
|
||||
leases_without_state, list_interfaces, merge_devices, query_to_device_query, resolve_devices,
|
||||
resolve_query, resolve_selector, resolve_wake_targets, wake_explicit, wake_from_query,
|
||||
wake_targets,
|
||||
};
|
||||
|
||||
pub type StatusResponse = Status<NeighborEntry>;
|
||||
|
||||
pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> {
|
||||
query_to_device_query(resolve_selector(input).await?)
|
||||
}
|
||||
|
||||
pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
|
||||
Ok(
|
||||
match wakey_linux::devices::classify_query(input.into()).await {
|
||||
QueryInput::Ip(ip_addr) => Query::Ip(ip_addr),
|
||||
QueryInput::Mac(mac_addr) => Query::Mac(mac_addr),
|
||||
QueryInput::Dev(dev) => Query::Interface(dev),
|
||||
QueryInput::Nud(state) => Query::NeighborState(state),
|
||||
QueryInput::Name(name) => Query::Text(name),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> {
|
||||
Ok(match query {
|
||||
Query::Ip(ip_addr) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
ips: vec![ip_addr],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Query::Mac(mac_addr) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
macs: vec![mac_addr],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Query::Interface(dev) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
devs: vec![dev],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Query::NeighborState(state) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
nuds: vec![state],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Query::Text(name) => DeviceQuery {
|
||||
name: Some(name),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
|
||||
let inventory = inventory(query.clone()).await?;
|
||||
let table = inventory
|
||||
.devices
|
||||
.iter()
|
||||
.flat_map(device_to_status_rows)
|
||||
.collect();
|
||||
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(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> {
|
||||
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
|
||||
.await
|
||||
.context("failed to read DHCP leases")?;
|
||||
if query.include_state {
|
||||
Ok(wakey_linux::dhcp::enrich_leases_with_nud_state(leases).await)
|
||||
} else {
|
||||
Ok(leases_without_state(leases))
|
||||
}
|
||||
}
|
||||
|
||||
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 targets = resolve_wake_targets(input).await?;
|
||||
wake_targets(targets).await
|
||||
}
|
||||
|
||||
pub async fn broadcast_wake_targets(mac: macaddr::MacAddr) -> Result<Vec<WakeTarget>> {
|
||||
Ok(get_interface_summaries()
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|iface| iface.addrs.into_iter())
|
||||
.filter_map(|addr| addr.broadcast)
|
||||
.map(|ip| WakeTarget {
|
||||
ip: Some(std::net::IpAddr::V4(ip)),
|
||||
mac: Some(mac),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn wake_explicit(mac: macaddr::MacAddr, ip: Option<std::net::IpAddr>) -> Result<WakeResult> {
|
||||
let targets = match ip {
|
||||
Some(ip) => vec![WakeTarget {
|
||||
ip: Some(ip),
|
||||
mac: Some(mac),
|
||||
}],
|
||||
None => broadcast_wake_targets(mac).await?,
|
||||
};
|
||||
wake_targets(targets).await
|
||||
}
|
||||
|
||||
pub async fn list_interfaces() -> Result<Vec<String>> {
|
||||
Ok(wakey_linux::devices::devs_sorted().await)
|
||||
}
|
||||
|
||||
pub async fn get_interface_summaries() -> Result<Vec<InterfaceSummary>> {
|
||||
wakey_linux::devices::list_interface_summaries().await
|
||||
}
|
||||
|
||||
pub async fn get_interface_summary(name: &str) -> Result<Option<InterfaceSummary>> {
|
||||
Ok(get_interface_summaries()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|iface| iface.ifname == name))
|
||||
}
|
||||
|
||||
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 async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
|
||||
let query = resolve_query(input).await?;
|
||||
inventory(query).await.map(|inventory| inventory.devices)
|
||||
}
|
||||
|
||||
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
|
||||
let neighbors = wakey_linux::devices::query_status(&query).await?;
|
||||
let leases = get_leases(LeaseQuery {
|
||||
include_state: false,
|
||||
})
|
||||
.await?;
|
||||
Ok(DeviceInventory {
|
||||
devices: merge_devices(neighbors, leases, &query),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn resolve_wake_targets(input: impl Into<String>) -> Result<Vec<WakeTarget>> {
|
||||
let devices = resolve_devices(input).await?;
|
||||
Ok(devices
|
||||
.into_iter()
|
||||
.flat_map(|device| {
|
||||
let mac = device.macs.first().copied();
|
||||
device
|
||||
.ips
|
||||
.into_iter()
|
||||
.map(move |ip| WakeTarget { ip: Some(ip), mac })
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn merge_devices(
|
||||
neighbors: Vec<NeighborEntry>,
|
||||
leases: Vec<DhcpLeaseWithState>,
|
||||
query: &DeviceQuery,
|
||||
) -> Vec<Device> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let mut by_mac: BTreeMap<String, (Vec<NeighborEntry>, Vec<DhcpLease>)> = BTreeMap::new();
|
||||
|
||||
for row in neighbors {
|
||||
let key = row
|
||||
.mac
|
||||
.map(|m| m.to_string())
|
||||
.unwrap_or_else(|| format!("ip:{}", row.ip));
|
||||
by_mac.entry(key).or_default().0.push(row);
|
||||
}
|
||||
for lease in leases {
|
||||
let key = lease.lease_line.mac.to_string();
|
||||
by_mac.entry(key).or_default().1.push(lease.lease_line);
|
||||
}
|
||||
|
||||
let mut devices: Vec<Device> = by_mac
|
||||
.into_values()
|
||||
.map(|(neighbors, leases)| Device::from_parts(neighbors, leases))
|
||||
.collect();
|
||||
|
||||
if let Some(name) = &query.name {
|
||||
devices.retain(|device| device.names.iter().any(|n| n == name));
|
||||
}
|
||||
if !query.filter.devs.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.interfaces
|
||||
.iter()
|
||||
.any(|iface| query.filter.devs.contains(iface))
|
||||
});
|
||||
}
|
||||
if !query.filter.ips.is_empty() {
|
||||
devices.retain(|device| device.ips.iter().any(|ip| query.filter.ips.contains(ip)));
|
||||
}
|
||||
if !query.filter.macs.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.macs
|
||||
.iter()
|
||||
.any(|mac| query.filter.macs.contains(mac))
|
||||
});
|
||||
}
|
||||
if !query.filter.nuds.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.neighbors
|
||||
.iter()
|
||||
.any(|neighbor| query.filter.nuds.contains(&neighbor.state))
|
||||
});
|
||||
}
|
||||
|
||||
devices.sort_by(|a, b| {
|
||||
presence_rank(b.presence)
|
||||
.cmp(&presence_rank(a.presence))
|
||||
.then_with(|| a.names.first().cmp(&b.names.first()))
|
||||
});
|
||||
devices
|
||||
}
|
||||
|
||||
const fn presence_rank(presence: Presence) -> u8 {
|
||||
match presence {
|
||||
Presence::Online => 3,
|
||||
Presence::LikelyOnline => 2,
|
||||
Presence::Unknown => 1,
|
||||
Presence::Offline => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn device_to_status_rows(device: &Device) -> Vec<NeighborEntry> {
|
||||
if !device.neighbors.is_empty() {
|
||||
return device.neighbors.clone();
|
||||
}
|
||||
|
||||
let fallback_mac = device.macs.first().copied();
|
||||
let fallback_dev = device.interfaces.first().cloned();
|
||||
let fallback_state = match device.presence {
|
||||
Presence::Online => wakey_core::NeighborState::Reachable,
|
||||
Presence::LikelyOnline => wakey_core::NeighborState::Stale,
|
||||
Presence::Offline => wakey_core::NeighborState::Failed,
|
||||
Presence::Unknown => wakey_core::NeighborState::None,
|
||||
};
|
||||
|
||||
device
|
||||
.ips
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|ip| NeighborEntry {
|
||||
ip,
|
||||
dev: fallback_dev.clone(),
|
||||
mac: fallback_mac,
|
||||
state: fallback_state,
|
||||
})
|
||||
.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};
|
||||
use wakey_core::{
|
||||
DhcpLease, NeighborEntry, NeighborState, Presence, Query, WakeStatus, WakeTarget,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_query_parses_ip() {
|
||||
@@ -386,7 +75,7 @@ mod tests {
|
||||
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
|
||||
state: NeighborState::Reachable,
|
||||
}];
|
||||
let leases = vec![DhcpLeaseWithState {
|
||||
let leases = vec![wakey_core::DhcpLeaseWithState {
|
||||
lease_line: DhcpLease {
|
||||
expires_epoch: 1,
|
||||
ip: IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)),
|
||||
@@ -395,7 +84,7 @@ mod tests {
|
||||
},
|
||||
nud_state: None,
|
||||
}];
|
||||
let devices = merge_devices(neighbors, leases, &DeviceQuery::default());
|
||||
let devices = merge_devices(neighbors, leases, &wakey_core::DeviceQuery::default());
|
||||
assert_eq!(devices.len(), 1);
|
||||
assert_eq!(devices[0].presence, Presence::Online);
|
||||
assert_eq!(devices[0].names, vec!["pc".to_string()]);
|
||||
|
||||
+4
-1
@@ -235,7 +235,10 @@ fn render_devs_table(devs: &[InterfaceSummary]) -> Table {
|
||||
table
|
||||
}
|
||||
|
||||
fn filter_interface_summaries(mut devs: Vec<InterfaceSummary>, args: &DevsArgs) -> Vec<InterfaceSummary> {
|
||||
fn filter_interface_summaries(
|
||||
mut devs: Vec<InterfaceSummary>,
|
||||
args: &DevsArgs,
|
||||
) -> Vec<InterfaceSummary> {
|
||||
if args.up {
|
||||
devs.retain(|dev| dev.operstate == "up");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use anyhow::Result;
|
||||
use wakey_core::InterfaceSummary;
|
||||
|
||||
pub async fn list_interfaces() -> Result<Vec<String>> {
|
||||
Ok(wakey_linux::devices::devs_sorted().await)
|
||||
}
|
||||
|
||||
pub async fn get_interface_summaries() -> Result<Vec<InterfaceSummary>> {
|
||||
wakey_linux::devices::list_interface_summaries().await
|
||||
}
|
||||
|
||||
pub async fn get_interface_summary(name: &str) -> Result<Option<InterfaceSummary>> {
|
||||
Ok(get_interface_summaries()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|iface| iface.ifname == name))
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use anyhow::Result;
|
||||
use wakey_core::{
|
||||
Device, DeviceInventory, DeviceQuery, DhcpLease, DhcpLeaseWithState, NeighborEntry, Presence,
|
||||
};
|
||||
|
||||
use crate::service::leases::get_leases;
|
||||
use crate::service::query::resolve_query;
|
||||
|
||||
pub async fn resolve_devices(input: impl Into<String>) -> Result<Vec<Device>> {
|
||||
let query = resolve_query(input).await?;
|
||||
inventory(query).await.map(|inventory| inventory.devices)
|
||||
}
|
||||
|
||||
pub async fn inventory(query: DeviceQuery) -> Result<DeviceInventory> {
|
||||
let neighbors = wakey_linux::devices::query_status(&query).await?;
|
||||
let leases = get_leases(wakey_core::LeaseQuery {
|
||||
include_state: false,
|
||||
})
|
||||
.await?;
|
||||
Ok(DeviceInventory {
|
||||
devices: merge_devices(neighbors, leases, &query),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn merge_devices(
|
||||
neighbors: Vec<NeighborEntry>,
|
||||
leases: Vec<DhcpLeaseWithState>,
|
||||
query: &DeviceQuery,
|
||||
) -> Vec<Device> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let mut by_mac: BTreeMap<String, (Vec<NeighborEntry>, Vec<DhcpLease>)> = BTreeMap::new();
|
||||
|
||||
for row in neighbors {
|
||||
let key = row
|
||||
.mac
|
||||
.map(|m| m.to_string())
|
||||
.unwrap_or_else(|| format!("ip:{}", row.ip));
|
||||
by_mac.entry(key).or_default().0.push(row);
|
||||
}
|
||||
for lease in leases {
|
||||
let key = lease.lease_line.mac.to_string();
|
||||
by_mac.entry(key).or_default().1.push(lease.lease_line);
|
||||
}
|
||||
|
||||
let mut devices: Vec<Device> = by_mac
|
||||
.into_values()
|
||||
.map(|(neighbors, leases)| Device::from_parts(neighbors, leases))
|
||||
.collect();
|
||||
|
||||
if let Some(name) = &query.name {
|
||||
devices.retain(|device| device.names.iter().any(|n| n == name));
|
||||
}
|
||||
if !query.filter.devs.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.interfaces
|
||||
.iter()
|
||||
.any(|iface| query.filter.devs.contains(iface))
|
||||
});
|
||||
}
|
||||
if !query.filter.ips.is_empty() {
|
||||
devices.retain(|device| device.ips.iter().any(|ip| query.filter.ips.contains(ip)));
|
||||
}
|
||||
if !query.filter.macs.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.macs
|
||||
.iter()
|
||||
.any(|mac| query.filter.macs.contains(mac))
|
||||
});
|
||||
}
|
||||
if !query.filter.nuds.is_empty() {
|
||||
devices.retain(|device| {
|
||||
device
|
||||
.neighbors
|
||||
.iter()
|
||||
.any(|neighbor| query.filter.nuds.contains(&neighbor.state))
|
||||
});
|
||||
}
|
||||
|
||||
devices.sort_by(|a, b| {
|
||||
presence_rank(b.presence)
|
||||
.cmp(&presence_rank(a.presence))
|
||||
.then_with(|| a.names.first().cmp(&b.names.first()))
|
||||
});
|
||||
devices
|
||||
}
|
||||
|
||||
const fn presence_rank(presence: Presence) -> u8 {
|
||||
match presence {
|
||||
Presence::Online => 3,
|
||||
Presence::LikelyOnline => 2,
|
||||
Presence::Unknown => 1,
|
||||
Presence::Offline => 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use anyhow::{Context, Result};
|
||||
use wakey_core::{DhcpLease, DhcpLeaseWithState, LeaseQuery};
|
||||
|
||||
pub async fn get_leases(query: LeaseQuery) -> Result<Vec<DhcpLeaseWithState>> {
|
||||
let leases = wakey_linux::dhcp::read_dhcp_leases_with_names()
|
||||
.await
|
||||
.context("failed to read DHCP leases")?;
|
||||
if query.include_state {
|
||||
Ok(wakey_linux::dhcp::enrich_leases_with_nud_state(leases).await)
|
||||
} else {
|
||||
Ok(leases_without_state(leases))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn leases_without_state(leases: Vec<DhcpLease>) -> Vec<DhcpLeaseWithState> {
|
||||
leases
|
||||
.into_iter()
|
||||
.map(|lease_line| DhcpLeaseWithState {
|
||||
lease_line,
|
||||
nud_state: None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
pub mod interfaces;
|
||||
pub mod inventory;
|
||||
pub mod leases;
|
||||
pub mod query;
|
||||
pub mod status;
|
||||
pub mod wake;
|
||||
|
||||
pub use interfaces::{get_interface_summaries, get_interface_summary, get_ips, list_interfaces};
|
||||
pub use inventory::{inventory, merge_devices, resolve_devices};
|
||||
pub use leases::{get_leases, leases_without_state};
|
||||
pub use query::{query_to_device_query, resolve_query, resolve_selector};
|
||||
pub use status::{StatusResponse, device_to_status_rows, get_status, get_status_for_input};
|
||||
pub use wake::{
|
||||
broadcast_wake_targets, resolve_wake_targets, wake_explicit, wake_from_query, wake_targets,
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
use anyhow::Result;
|
||||
use wakey_core::{DeviceFilters, DeviceQuery, Query, QueryInput};
|
||||
|
||||
pub async fn resolve_query(input: impl Into<String>) -> Result<DeviceQuery> {
|
||||
query_to_device_query(resolve_selector(input).await?)
|
||||
}
|
||||
|
||||
pub async fn resolve_selector(input: impl Into<String>) -> Result<Query> {
|
||||
Ok(
|
||||
match wakey_linux::devices::classify_query(input.into()).await {
|
||||
QueryInput::Ip(ip_addr) => Query::Ip(ip_addr),
|
||||
QueryInput::Mac(mac_addr) => Query::Mac(mac_addr),
|
||||
QueryInput::Dev(dev) => Query::Interface(dev),
|
||||
QueryInput::Nud(state) => Query::NeighborState(state),
|
||||
QueryInput::Name(name) => Query::Text(name),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn query_to_device_query(query: Query) -> Result<DeviceQuery> {
|
||||
Ok(match query {
|
||||
Query::Ip(ip_addr) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
ips: vec![ip_addr],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Query::Mac(mac_addr) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
macs: vec![mac_addr],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Query::Interface(dev) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
devs: vec![dev],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Query::NeighborState(state) => DeviceQuery {
|
||||
filter: DeviceFilters {
|
||||
nuds: vec![state],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Query::Text(name) => DeviceQuery {
|
||||
name: Some(name),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use anyhow::Result;
|
||||
use wakey_core::{Device, DeviceQuery, NeighborEntry, Presence, Status};
|
||||
|
||||
use crate::service::inventory::inventory;
|
||||
use crate::service::query::resolve_query;
|
||||
|
||||
pub type StatusResponse = Status<NeighborEntry>;
|
||||
|
||||
pub async fn get_status(query: DeviceQuery) -> Result<StatusResponse> {
|
||||
let inventory = inventory(query.clone()).await?;
|
||||
let table = inventory
|
||||
.devices
|
||||
.iter()
|
||||
.flat_map(device_to_status_rows)
|
||||
.collect();
|
||||
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 fn device_to_status_rows(device: &Device) -> Vec<NeighborEntry> {
|
||||
if !device.neighbors.is_empty() {
|
||||
return device.neighbors.clone();
|
||||
}
|
||||
|
||||
let fallback_mac = device.macs.first().copied();
|
||||
let fallback_dev = device.interfaces.first().cloned();
|
||||
let fallback_state = match device.presence {
|
||||
Presence::Online => wakey_core::NeighborState::Reachable,
|
||||
Presence::LikelyOnline => wakey_core::NeighborState::Stale,
|
||||
Presence::Offline => wakey_core::NeighborState::Failed,
|
||||
Presence::Unknown => wakey_core::NeighborState::None,
|
||||
};
|
||||
|
||||
device
|
||||
.ips
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|ip| NeighborEntry {
|
||||
ip,
|
||||
dev: fallback_dev.clone(),
|
||||
mac: fallback_mac,
|
||||
state: fallback_state,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use anyhow::{Context, Result};
|
||||
use macaddr::MacAddr;
|
||||
use std::net::IpAddr;
|
||||
use wakey_core::{WakeResult, WakeTarget};
|
||||
|
||||
use crate::service::interfaces::get_interface_summaries;
|
||||
use crate::service::inventory::resolve_devices;
|
||||
|
||||
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 targets = resolve_wake_targets(input).await?;
|
||||
wake_targets(targets).await
|
||||
}
|
||||
|
||||
pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> {
|
||||
Ok(get_interface_summaries()
|
||||
.await?
|
||||
.into_iter()
|
||||
.flat_map(|iface| iface.addrs.into_iter())
|
||||
.filter_map(|addr| addr.broadcast)
|
||||
.map(|ip| WakeTarget {
|
||||
ip: Some(IpAddr::V4(ip)),
|
||||
mac: Some(mac),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResult> {
|
||||
let targets = match ip {
|
||||
Some(ip) => vec![WakeTarget {
|
||||
ip: Some(ip),
|
||||
mac: Some(mac),
|
||||
}],
|
||||
None => broadcast_wake_targets(mac).await?,
|
||||
};
|
||||
wake_targets(targets).await
|
||||
}
|
||||
|
||||
pub async fn resolve_wake_targets(input: impl Into<String>) -> Result<Vec<WakeTarget>> {
|
||||
let devices = resolve_devices(input).await?;
|
||||
Ok(devices
|
||||
.into_iter()
|
||||
.flat_map(|device| {
|
||||
let mac = device.macs.first().copied();
|
||||
device
|
||||
.ips
|
||||
.into_iter()
|
||||
.map(move |ip| WakeTarget { ip: Some(ip), mac })
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ use tokio::{
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use crate::{arpparse::NUDState, utils::query::get_mac};
|
||||
use crate::{legacy::arpparse::NUDState, utils::query::get_mac};
|
||||
|
||||
pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
|
||||
timeout(Duration::from_secs(1), TcpStream::connect(addr))
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ pub mod macs {
|
||||
use anyhow::Result;
|
||||
use std::net::IpAddr;
|
||||
|
||||
use crate::arpparse::{IpNeighLine, NUDState};
|
||||
use crate::legacy::arpparse::{IpNeighLine, NUDState};
|
||||
|
||||
pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
|
||||
wakey_linux::devices::get_ips(machine_name).await
|
||||
|
||||
Reference in New Issue
Block a user