13 Commits
41 changed files with 740 additions and 639 deletions

No files matched your search

+5
View File
@@ -3,3 +3,8 @@ target = "armv7-unknown-linux-musleabihf"
[target.armv7-unknown-linux-musleabihf] [target.armv7-unknown-linux-musleabihf]
linker = "rust-lld" linker = "rust-lld"
[alias]
ldabr = "b -r --target=target.armv7-unknown-linux-musleabihf"
t = "test -- --nocapture --test-threads=1"
tdebug = "test -- --nocapture --test-threads=1 --show-output"
+7 -2
View File
@@ -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.
+2 -1
View File
@@ -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
+1 -1
View File
@@ -1179,7 +1179,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "wakey" name = "wakey"
version = "0.1.2" version = "0.1.6"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
+9 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "wakey" name = "wakey"
version = "0.1.2" version = "0.1.6"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
@@ -16,7 +16,12 @@ serde_json = "1.0.143"
serde_with = { version = "3.14.0", features = ["json"] } serde_with = { version = "3.14.0", features = ["json"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] } strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
thiserror = "2.0.16" thiserror = "2.0.16"
tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] } tokio = { version = "1.47.1", features = [
"fs",
"process",
"rt-multi-thread",
"io-util",
] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
[profile.release] [profile.release]
@@ -25,4 +30,5 @@ strip = true
[features] [features]
default = ["very-smart-parsing"] default = ["very-smart-parsing"]
very-smart-parsing = [] # this is the a-bit-redundant parse thing that copilot made very-smart-parsing = [
] # this is the a-bit-redundant parse thing that copilot made
+5 -5
View File
@@ -10,12 +10,12 @@
START=50 # Run before Tailscale (typically START~80) START=50 # Run before Tailscale (typically START~80)
start() { start() {
LOGFILE=/var/log/update_tailscale.log LOGFILE=/var/log/update_tailscale.log
MAX_RETRIES=10 MAX_RETRIES=10
RETRY_COUNT=0 RETRY_COUNT=0
# shellcheck disable=SC2016 # shellcheck disable=SC2016
nohup sh -c ' sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if fn; then if fn; then
echo "[update_tailscale] Success at $(date)" >>"$LOGFILE" echo "[update_tailscale] Success at $(date)" >>"$LOGFILE"
+5 -5
View File
@@ -2,12 +2,12 @@
START=66 START=66
start() { start() {
LOGFILE=/var/log/update_wakey.log LOGFILE=/var/log/update_wakey.log
MAX_RETRIES=10 MAX_RETRIES=10
RETRY_COUNT=0 RETRY_COUNT=0
# shellcheck disable=SC2016 # shellcheck disable=SC2016
nohup sh -c ' sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if /etc/ldlda_help/update_wakey.sh; then if /etc/ldlda_help/update_wakey.sh; then
echo "[update_wakey] Success at $(date)" >>"$LOGFILE" echo "[update_wakey] Success at $(date)" >>"$LOGFILE"
+7 -7
View File
@@ -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
} }
+2 -2
View File
@@ -6,7 +6,7 @@ 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
@@ -18,7 +18,7 @@ kill -TERM "$pids" 2>/dev/null || true
usleep 250000 # uhhh sleep is stupid 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
+8 -8
View File
@@ -3,22 +3,22 @@
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
if [ -f /tmp/resolv.conf ]; then if [ -f /tmp/resolv.conf ]; then
cp -af /tmp/resolv.conf /etc/resolv.conf cp -af /tmp/resolv.conf /etc/resolv.conf
elif [ -f /rom/etc/resolv.conf ]; then elif [ -f /rom/etc/resolv.conf ]; then
cp -af /rom/etc/resolv.conf /etc/resolv.conf cp -af /rom/etc/resolv.conf /etc/resolv.conf
fi fi
if [ -x "$(which tailscale)" ]; then if [ -x "$(which tailscale)" ]; then
tailscale update tailscale update
exit 0 exit 0
fi 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}"
@@ -26,8 +26,8 @@ URL="https://pkgs.tailscale.com/stable/${LATEST}"
cd /var/tmp cd /var/tmp
if [ -f "$LATEST" ]; then if [ -f "$LATEST" ]; then
echo "file exists" echo "file exists"
rm "$LATEST" rm "$LATEST"
fi fi
echo "Downloading $LATEST..." echo "Downloading $LATEST..."
+88 -88
View File
@@ -24,113 +24,113 @@ STAGING="$TMPDIR/wakey-rootfs.$$.$ARCH"
log() { echo "[update_wakey] $*"; } log() { echo "[update_wakey] $*"; }
fail() { fail() {
echo "[update_wakey] ERROR: $*" >&2 echo "[update_wakey] ERROR: $*" >&2
exit 1 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
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 first 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 | head -n1 |
cut -d '"' -f 4 cut -d '"' -f 4
} }
main() { main() {
[ -f "$FLAG" ] && return [ -f "$FLAG" ] && return
URL="${WAKEY_TGZ_URL:-}" URL="${WAKEY_TGZ_URL:-}"
if [ -z "$URL" ]; then if [ -z "$URL" ]; then
HOST=${WAKEY_HOST:-git.ldlda.com} HOST=${WAKEY_HOST:-git.ldlda.com}
OWNER=${WAKEY_OWNER:-lda} OWNER=${WAKEY_OWNER:-lda}
REPO=${WAKEY_REPO:-wakey} REPO=${WAKEY_REPO:-wakey}
if [ -n "${WAKEY_VERSION:-}" ]; then if [ -n "${WAKEY_VERSION:-}" ]; then
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz" FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE" URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
else else
URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL" URL=$(latest_asset_url "$HOST" "$OWNER" "$REPO" "$ARCH") || fail "unable to resolve latest asset URL"
[ -n "$URL" ] || fail "no asset URL found for arch $ARCH" [ -n "$URL" ] || fail "no asset URL found for arch $ARCH"
fi 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"
mkdir -p "$TMPDIR" "$STAGING" mkdir -p "$TMPDIR" "$STAGING"
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed" tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
# Ensure execute bits on staged files we know should be executable # Ensure execute bits on staged files we know should be executable
for f in \ for f in \
"$STAGING/etc/init.d/"* \ "$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \ "$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/wakey" \ "$STAGING/root/.bin/wakey" \
"$STAGING/root/.bin/kill_wakey.sh" \ "$STAGING/root/.bin/kill_wakey.sh" \
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do "$STAGING/root/.bin/remote_deploy_wakey.sh"; do
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true [ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
done done
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt) # Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
for f in \ for f in \
"$STAGING/etc/init.d/"* \ "$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \ "$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/"*.sh; do "$STAGING/root/.bin/"*.sh; do
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true [ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
done done
# Copy staged tree into / # Copy staged tree into /
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed" tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
rm -f "$TMPFILE" rm -f "$TMPFILE"
rm -rf "$STAGING" rm -rf "$STAGING"
if [ -f /etc/init.d/wakey ]; then if [ -f /etc/init.d/wakey ]; then
/etc/init.d/wakey enable || true /etc/init.d/wakey enable || true
/etc/init.d/wakey restart || /etc/init.d/wakey start || true /etc/init.d/wakey restart || /etc/init.d/wakey start || true
fi fi
touch "$FLAG" touch "$FLAG"
log "done" log "done"
} }
main "$@" main "$@"
+1 -1
View File
@@ -52,7 +52,7 @@ class RsAssetModule:
body = "\n".join(files + subs) body = "\n".join(files + subs)
return ( return (
f"pub mod {sanitize(self.folder.name).lower()} {{" * self.full f"pub mod {sanitize(self.folder.name).lower()} {{" * self.full
+ f"\n{indent(body, 4*self.full)}\n" + f"\n{indent(body, 4 * self.full)}\n"
+ self.full * "}" + self.full * "}"
) )
+14 -14
View File
@@ -10,32 +10,32 @@ INIT="/etc/init.d/wakey"
KILL="/root/.bin/kill_wakey.sh" KILL="/root/.bin/kill_wakey.sh"
if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2 echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
exit 2 exit 2
fi fi
[ -f "$BIN_TMP" ] || { [ -f "$BIN_TMP" ] || {
echo "tmp binary not found: $BIN_TMP" >&2 echo "tmp binary not found: $BIN_TMP" >&2
exit 1 exit 1
} }
chmod +x "$BIN_TMP" || true chmod +x "$BIN_TMP" || true
if [ "$RESTART" = "1" ]; then if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then if [ -x "$INIT" ]; then
"$INIT" stop || true "$INIT" stop || true
elif [ -x "$KILL" ]; then elif [ -x "$KILL" ]; then
sh "$KILL" || true sh "$KILL" || true
fi fi
fi fi
mv -f "$BIN_TMP" "$DEST" mv -f "$BIN_TMP" "$DEST"
if [ "$RESTART" = "1" ]; then if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then if [ -x "$INIT" ]; then
"$INIT" start || "$INIT" restart || true "$INIT" start || "$INIT" restart || true
else else
nohup "$DEST" >/dev/null 2>&1 & nohup "$DEST" >/dev/null 2>&1 &
fi fi
fi fi
exit 0 exit 0
File renamed without changes.
+2 -1
View File
@@ -2,7 +2,8 @@
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json"; const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
/// Load MAC->name cache from disk /// Load MAC->name cache from disk
async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> { pub(crate) async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>>
{
match tokio::fs::read_to_string(MAC_NAME_CACHE).await { match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other), Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()), Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()),
+12 -8
View File
@@ -1,7 +1,14 @@
use axum::{ //! braindead version v0.1.x
Router, //!
routing::{get, post}, //! # whats next
}; //!
//! for version 2 i hope to have:
//!
//! 1. idk reworked frontend;
//! 2. incorporate ip -j;
//! 3. small 1-5 second caching;
use axum::{Router, routing::get};
use tokio::net::TcpListener; use tokio::net::TcpListener;
mod arpparse; mod arpparse;
pub mod assets; pub mod assets;
@@ -10,18 +17,15 @@ mod route;
mod utils; mod utils;
use std::io; use std::io;
const MACHINE_NAME: &str = "lda.lan";
#[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_router, home_2, home_2_route, wake_handler}; use crate::route::{api_router, home_2, home_2_route};
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()); .nest("/api", api_router());
-99
View File
@@ -1,99 +0,0 @@
pub mod api;
pub mod devs;
pub mod dhcp;
pub mod status;
pub mod wake;
pub use crate::route::api::{DeviceQuery, api_router};
use crate::{
assets::{self},
utils::{ping::_ping_ip, wake::wake},
};
use axum::{
Router,
http::{StatusCode, header},
response::{Html, IntoResponse},
routing::get,
};
use axum_extra::extract::Query;
use crate::utils::route::serve_js;
use crate::{MACHINE_NAME, utils::_status_build};
pub async fn wake_handler(
Query(DeviceQuery { name, .. }): Query<DeviceQuery>,
) -> axum::response::Result<impl IntoResponse> {
match wake(name.as_deref().unwrap_or(MACHINE_NAME)).await {
Ok(0) => Err((StatusCode::NOT_FOUND, "No packets sent!").into()),
Ok(x) => Ok((
StatusCode::ACCEPTED,
format!("{x} packet{s} sent!", s = if x > 1 { "s" } else { "" }),
)),
_ => Err((StatusCode::GATEWAY_TIMEOUT, "Wake failed").into()),
}
}
pub async fn _get_status_2(q: Query<DeviceQuery>) -> Html<String> {
let name = match q {
Query(DeviceQuery {
name: Some(name), ..
}) => name,
_ => MACHINE_NAME.to_string(),
};
Html(_status_build(&name).await)
}
pub async fn _home() -> Html<String> {
Html(format!(
r#"
<html>
<body>
<p><a href="/home_2">Alternate UI</a></p>
<p>the machine is {}! <a href="/status">Status</a></p>
<form method="POST" action="/wake">
<button type="submit">Wake LDA</button>
</form>
</body>
</html>
"#,
if _ping_ip((MACHINE_NAME, 22)).await {
"on"
} else {
"off"
} // match get_ips(MACHINE_NAME).await {
// Ok(ips) => {
// // let addrs: Vec<SocketAddr> = ips.into_iter().map(|ip|(ip, 22).into()).collect();
// }
// Err(_) => "off",
// }
))
}
pub async fn home_2() -> Html<&'static str> {
Html(assets::HOME_2_HTML)
}
pub fn home_2_route() -> Router {
use assets::*;
Router::new()
.route("/home_2", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2/", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2.html", get(|| async { Html(HOME_2_HTML) }))
.route(
"/home_2/styles.css",
get(|| async {
(
[
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
home_2::STYLES_CSS,
)
}),
)
.route("/home_2/main.js", get(|| serve_js(home_2::MAIN_JS)))
.route("/home_2/leases.js", get(|| serve_js(home_2::LEASES_JS)))
.route("/home_2/status.js", get(|| serve_js(home_2::STATUS_JS)))
.route("/home_2/utils.js", get(|| serve_js(home_2::UTILS_JS)))
.route("/home_2/wake.js", get(|| serve_js(home_2::WAKE_JS)))
.route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS)))
}
+52 -19
View File
@@ -1,32 +1,63 @@
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::routing::post; use axum::{extract::Path, response::Redirect};
use axum::{Router, extract::Path, response::Redirect, routing::get};
use crate::utils::route; use crate::route::status::{DeviceQuery, Filters, NamePath};
use crate::utils::query::get_ips;
// Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly // 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> {
match serde_html_form::to_string(route::status_smart_redirect(q).await) { // no less bullshit
let query = match parse_query(q) {
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}"))), Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
Err(e) => Err(( Err(e) => Err((
StatusCode::BAD_GATEWAY, StatusCode::BAD_GATEWAY,
Json(StatusError { Json(ApiError {
error: e.to_string(), error: e.to_string(),
..Default::default()
}), }),
)), )),
} }
} }
pub use crate::route::devs::*;
pub use crate::route::dhcp::*;
pub use crate::route::status::*;
pub use crate::route::wake::*;
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect { pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
Redirect::permanent(&format!( Redirect::permanent(&format!(
"/api/status?name={name}", "/api/status?name={name}",
@@ -34,12 +65,14 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
)) ))
} }
pub fn api_router() -> Router { pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
Router::new() get_ips(&name).await.map_or_else(
.route("/status/{name}", get(status_redirect)) |e| {
.route("/status", get(get_status_json)) ApiError {
.route("/dhcp_leases", get(get_dhcp_leases)) error: e.to_string(),
.route("/smart/{q}", get(status_smart_redirect)) }
.route("/devs", get(devs_router)) .into_response()
.route("/wake", post(wake_multi)) },
|ips| Json(ips.collect::<Vec<_>>()).into_response(),
)
} }
+8 -5
View File
@@ -1,4 +1,8 @@
use crate::{dhcpparse, route::api::StatusError, utils::parse::boolish_str}; use crate::{
dhcpparse::read_dhcp_leases_with_names,
route::error::ApiError,
utils::{parse::boolish_str, query::enrich_leases_with_nud_state},
};
use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse}; use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
// DHCP lease endpoints // DHCP lease endpoints
@@ -14,19 +18,18 @@ pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl Into
.map(boolish_str) .map(boolish_str)
.unwrap_or(false); .unwrap_or(false);
match dhcpparse::read_dhcp_leases_with_names().await { match read_dhcp_leases_with_names().await {
Ok(leases_with_names) => { Ok(leases_with_names) => {
if !include_state { if !include_state {
return (StatusCode::OK, Json(leases_with_names)).into_response(); return (StatusCode::OK, Json(leases_with_names)).into_response();
} }
let out = crate::utils::query::enrich_leases_with_nud_state(leases_with_names).await; let out = enrich_leases_with_nud_state(leases_with_names).await;
(StatusCode::OK, Json(out)).into_response() (StatusCode::OK, Json(out)).into_response()
} }
Err(e) => ( Err(e) => (
StatusCode::BAD_GATEWAY, StatusCode::BAD_GATEWAY,
Json(StatusError { Json(ApiError {
error: e.to_string(), error: e.to_string(),
..Default::default()
}), }),
) )
.into_response(), .into_response(),
+17
View File
@@ -0,0 +1,17 @@
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ApiError {
pub error: String,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response()
}
}
+72
View File
@@ -0,0 +1,72 @@
pub mod api;
pub mod devs;
pub mod dhcp;
pub mod error;
pub mod status;
pub mod wake;
use crate::assets;
use crate::dhcpparse::load_mac_name_cache;
use crate::route::api::ip;
use crate::route::api::status_redirect;
use crate::route::api::status_smart_redirect;
use crate::route::devs::devs_router;
use crate::route::dhcp::get_dhcp_leases;
use crate::route::status::get_status_json;
use crate::route::wake::wake_multi;
use axum::Json;
use axum::response::IntoResponse;
use axum::routing::post;
use axum::{Router, http::header, response::Html, routing::get};
use crate::utils::route::serve_js;
pub async fn home_2() -> Html<&'static str> {
Html(assets::HOME_2_HTML)
}
pub fn home_2_route() -> Router {
use assets::*;
Router::new()
.route("/home_2", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2/", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2.html", get(|| async { Html(HOME_2_HTML) }))
.route(
"/home_2/styles.css",
get(|| async {
(
[
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
home_2::STYLES_CSS,
)
}),
)
.route("/home_2/main.js", get(|| serve_js(home_2::MAIN_JS)))
.route("/home_2/leases.js", get(|| serve_js(home_2::LEASES_JS)))
.route("/home_2/status.js", get(|| serve_js(home_2::STATUS_JS)))
.route("/home_2/utils.js", get(|| serve_js(home_2::UTILS_JS)))
.route("/home_2/wake.js", get(|| serve_js(home_2::WAKE_JS)))
.route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS)))
}
pub fn api_router() -> Router {
Router::new()
.route("/status/{name}", get(status_redirect))
.route("/status", get(get_status_json))
.route("/dhcp_leases", get(get_dhcp_leases))
.route("/smart/{q}", get(status_smart_redirect))
.route("/devs", get(devs_router))
.route("/wake", post(wake_multi))
.route("/ips/{name}", get(ip))
.route(
"/mac-cache",
get(|| async {
match load_mac_name_cache().await {
Ok(h) => Json(h).into_response(),
Err(e) => e.to_string().into_response(),
}
}),
)
}
+41 -79
View File
@@ -1,30 +1,21 @@
use crate::{ use crate::route::error::ApiError;
arpparse::NUDState,
utils::parse::{de_many, serialize_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;
use macaddr::MacAddr; use macaddr::MacAddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::collections::HashSet;
use std::net::IpAddr; 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)] #[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
pub struct DeviceQuery { pub struct DeviceQuery {
pub name: Option<String>, pub name: Option<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde(flatten)]
pub ip: Vec<IpAddr>, pub filter: Filters,
#[serde(
default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs"
)]
pub mac: Vec<MacAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub dev: Vec<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub nud: Vec<NUDState>,
} }
#[derive(Debug, Default, Clone, Hash, Deserialize)] #[derive(Debug, Default, Clone, Hash, Deserialize)]
@@ -40,79 +31,51 @@ pub struct Status<T> {
pub filters: Filters, pub filters: Filters,
} }
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct Filters { pub struct Filters {
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub ip: Vec<IpAddr>, pub ips: Vec<IpAddr>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub dev: Vec<String>, pub devs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub nud: Vec<NUDState>, pub nuds: Vec<NUDState>,
#[serde( #[serde(
skip_serializing_if = "Vec::is_empty", default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs" serialize_with = "serialize_macs"
)] )]
pub mac: Vec<MacAddr>, pub macs: Vec<MacAddr>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Default)]
pub struct StatusError {
pub name: Option<String>,
pub error: String,
} }
pub async fn get_status_json( pub async fn get_status_json(
Query(DeviceQuery { Query(DeviceQuery {
name, name,
ip, filter:
dev, Filters {
nud, ips,
mac, devs,
nuds,
macs,
},
.. ..
}): Query<DeviceQuery>, }): Query<DeviceQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> { match get_macs(
if slice.is_empty() { &name.iter().collect::<Vec<_>>(),
vec![None] &ips,
} else { &devs.iter().collect::<Vec<_>>(),
slice.iter().cloned().map(Some).collect() &nuds,
} &macs,
} )
let ips_opt = if ip.is_empty() { .await
None
} else {
Some(ip.clone())
};
let dev_opts: Vec<Option<String>> = to_opts(&dev);
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
let filters = Filters {
ip,
dev,
nud,
mac: mac.clone(),
};
let mut tasks = Vec::new();
for d in &dev_opts {
for n in &nud_opts {
tasks.push(crate::utils::query::get_macs(
name.as_deref(),
ips_opt.as_deref(),
d.as_deref(),
*n,
));
}
}
// why try join all?
match futures::future::try_join_all(tasks)
.await
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
{ {
Ok(mut table) => { Ok(table) => {
if !mac.is_empty() { let filters = Filters {
let wanted: HashSet<MacAddr> = mac.into_iter().collect(); ips,
table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false)); devs,
} nuds,
macs,
};
( (
StatusCode::OK, StatusCode::OK,
Json(Status { Json(Status {
@@ -125,8 +88,7 @@ pub async fn get_status_json(
} }
Err(error) => ( Err(error) => (
StatusCode::BAD_GATEWAY, StatusCode::BAD_GATEWAY,
Json(StatusError { Json(ApiError {
name,
error: error.to_string(), error: error.to_string(),
}), }),
) )
+18 -21
View File
@@ -7,9 +7,11 @@ use crate::route::api::Status; */
use crate::utils::parse::mac::{des_opm, ser_opm}; use crate::utils::parse::mac::{des_opm, ser_opm};
use crate::utils::wake::wake_one; use crate::utils::wake::wake_one;
use axum::{extract::Json, http::StatusCode, response::IntoResponse}; use axum::{extract::Json, http::StatusCode, response::IntoResponse};
use futures::TryFutureExt;
use macaddr::MacAddr; use macaddr::MacAddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use tokio::net::UdpSocket;
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Default, Serialize, Clone)] #[derive(Debug, Default, Serialize, Clone)]
@@ -21,9 +23,8 @@ pub struct WakeResult {
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Clone, Copy)] #[derive(Debug, Serialize, Clone, Copy)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub ip: Option<IpAddr>, #[serde(flatten)]
#[serde(serialize_with = "ser_opm")] pub target: WakeTarget,
pub mac: Option<MacAddr>,
pub status: WakeTargetStatus, pub status: WakeTargetStatus,
} }
@@ -39,11 +40,11 @@ pub enum WakeTargetStatus {
} }
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Deserialize, Clone, Copy)] #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct WakeTarget { pub struct WakeTarget {
#[serde(default)] #[serde(default)]
pub ip: Option<IpAddr>, pub ip: Option<IpAddr>,
#[serde(default, deserialize_with = "des_opm")] #[serde(default, serialize_with = "ser_opm", deserialize_with = "des_opm")]
pub mac: Option<MacAddr>, pub mac: Option<MacAddr>,
} }
@@ -75,24 +76,20 @@ pub async fn wake_multi(Json(req): Json<Vec<WakeTarget>>) -> impl IntoResponse {
pub async fn wake_multi_split( pub async fn wake_multi_split(
targets: impl IntoIterator<Item = WakeTarget>, targets: impl IntoIterator<Item = WakeTarget>,
) -> io::Result<Vec<WakeTargetResult>> { ) -> io::Result<Vec<WakeTargetResult>> {
let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await?; let sock = UdpSocket::bind("[::]:0")
.or_else(|_| UdpSocket::bind(":0"))
.await?;
sock.set_broadcast(true)?; sock.set_broadcast(true)?;
Ok( let iter = targets.into_iter().map(async |c| {
futures::future::join_all(targets.into_iter().map(async |c| { if c.is_incomplete() {
if c.is_incomplete() { c.to_incomplete()
c.to_incomplete() } else {
} else { let t = c.try_into().expect("complete struct failed to try_into");
wake_one( wake_one(&sock, t).await.into()
&sock, }
c.try_into().expect("complete struct failed to try_into"), });
) Ok(futures::future::join_all(iter).await)
.await
.into()
}
}))
.await,
)
} }
/* #[derive(Debug, Serialize)] /* #[derive(Debug, Serialize)]
pub struct WakeStatusLine { pub struct WakeStatusLine {
-64
View File
@@ -1,64 +0,0 @@
// pub const LDA_MACS: [[u8; 6]; 2] = [
// // ether
// [0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
// // wifi
// [0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
// ];
/// is it time to lookup host lda.lan for this...
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub mod wake;
use crate::utils::query::_get_macs_2_1;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
pub mod query;
pub mod route;
// no custom ip deserializer needed when using axum_extra::extract::Query
// but we add a generic one to ignore blanks and accept OneOrMany
pub(crate) mod parse;
pub async fn _status_build(machine_name: &str) -> String {
let formatted_macs = match _get_macs_2_1(machine_name).await {
Ok(table) => {
let the: String = table
.iter()
.map(|(ip, mac, state)| {
let mac_str = // if let Some(mac) = mac {
mac.to_string()
// } else {
// "None".into()
// }
;
format!(
"<tr><td>{ip}</td><td>{mac_str}</td><td>{state}</td></tr>",
state = state._dumber_state()
)
})
.collect();
format!(
r#"<p>info of {machine_name}:</p>
<table>
<tr><th>IP</th><th>MAC</th><th>State</th></tr>
{the}
</table>"#
)
}
Err(e) => format!("<p>errors getting table for {machine_name}: {e}</p>"),
};
format!(
r#"
<html>
<body>
{formatted_macs}
</body>
</html>
"#,
)
}
+23
View File
@@ -0,0 +1,23 @@
// 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;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
pub mod query;
pub mod query_parser;
pub mod route;
// no custom ip deserializer needed when using axum_extra::extract::Query
// but we add a generic one to ignore blanks and accept OneOrMany
pub(crate) mod parse;
+2 -2
View File
@@ -7,7 +7,7 @@ use tokio::{
time::timeout, time::timeout,
}; };
use crate::{arpparse::NUDState, utils::query::get_macs}; use crate::{arpparse::NUDState, utils::query::get_mac};
pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool { 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))
@@ -19,7 +19,7 @@ pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
} }
pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 { pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 {
match get_macs(None, Some(&[addr.into()]), None, None).await { match get_mac(Some(addr.into()), None, &[] as &[NUDState]).await {
Err(_) => 0, Err(_) => 0,
Ok(l) => l Ok(l) => l
.into_iter() .into_iter()
+12 -2
View File
@@ -1,10 +1,20 @@
use std::collections::HashSet; use std::{collections::HashSet, fs};
pub fn get_dev() -> HashSet<String> { pub fn get_dev() -> HashSet<String> {
let mut devs: HashSet<String> = HashSet::new(); let mut devs: HashSet<String> = HashSet::new();
if let Ok(rd) = std::fs::read_dir("/sys/class/net") { if let Ok(rd) = std::fs::read_dir("/sys/class/net") {
for e in rd.flatten() { for e in rd.flatten() {
if let Ok(name) = e.file_name().into_string() 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 != "lo"
&& !name.is_empty() && !name.is_empty()
{ {
+2 -2
View File
@@ -1,5 +1,6 @@
use crate::arpparse::NUDState; use crate::arpparse::NUDState;
use crate::dhcpparse::DhcpLeaseLine; use crate::dhcpparse::DhcpLeaseLine;
use crate::utils::query::get_macs;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::net::IpAddr; use std::net::IpAddr;
@@ -14,11 +15,10 @@ pub struct DhcpLeaseOut {
/// Enrich DHCP leases with NUD state and rank using get_macs /// Enrich DHCP leases with NUD state and rank using get_macs
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> { pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
use crate::utils::query::macs::get_macs;
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect(); let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> = let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> =
std::collections::HashMap::new(); std::collections::HashMap::new();
if let Ok(rows) = get_macs(None, Some(&ips), None, None).await { if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await {
for row in rows { for row in rows {
let state = row.state; let state = row.state;
let r = state.rank(); let r = state.rank();
+95 -82
View File
@@ -1,13 +1,14 @@
use macaddr::MacAddr;
use crate::arpparse::{self, IpNeighLine, NUDState}; use crate::arpparse::{self, IpNeighLine, NUDState};
use crate::utils::{ use crate::utils::{
cmd::exec_command, cmd::exec_command,
error::{self, Error, Result}, error::{self, Result},
}; };
use macaddr::MacAddr;
use std::collections::HashSet; use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
pub async fn get_ips(machine_name: &str) -> error::Result<impl Iterator<Item = IpAddr>> { pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
Ok(tokio::net::lookup_host((machine_name, 0)) Ok(tokio::net::lookup_host((machine_name, 0))
.await .await
.map_err(|e| error::Error::DnsResolve { .map_err(|e| error::Error::DnsResolve {
@@ -17,92 +18,102 @@ pub async fn get_ips(machine_name: &str) -> error::Result<impl Iterator<Item = I
.map(|c| c.ip())) .map(|c| c.ip()))
} }
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> { // #[deprecated(
Ok(get_macs_1(machine_name) // since = "0.1.5",
.await? // note = "just call once everything with get mac
.into_iter() // and then filter it bro WHY DO YOU EVEN DO TS"
.filter_map( // )]
|IpNeighLine { // good now
ip, //
dev: _, // Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field.
mac, // This is because missing dev/mac usually means the entry is incomplete/transient.
state, //
}| mac.map(|mac| (ip, mac, state)), // when there is only one MACs (getmac got some), the result will not have them fields.
) // so there are three cases:
.collect()) //
} // 1. dont got nothing: take all of them (macset.is_empty())
// 2. exactly one: pre-filtered by ip, everything matches,
pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> { // devset.len() != 1 returns false, but then it works????
Ok(get_macs_1(machine_name) // OH THIS fuckass code i added it in the get_mac
.await? // 3. devset.len() > 1. if none then absolutely not match,
.into_iter() // if some then check with the set; thats normal
.filter_map(
|IpNeighLine {
ip: _,
dev: _,
mac,
state: _,
}| mac,
)
.collect())
}
pub async fn get_macs_1(machine_name: &str) -> Result<Vec<arpparse::IpNeighLine>> {
let dev = "br-lan";
let ips = get_ips(machine_name).await?;
let futures = ips.map(|ip| {
let ip = ip.to_canonical();
async move {
let cmd = "ip";
let args = ["neigh", "show", "to", &ip.to_string(), "dev", dev];
let o = exec_command(cmd, args).await?;
if !o.status.success() {
return Err(Error::CommandFailed {
cmd,
args: args.iter().map(ToString::to_string).collect(),
status: o.status.code(),
stderr: String::from_utf8_lossy(&o.stderr).into(),
});
};
Ok(String::from_utf8_lossy(&o.stdout)
.lines()
.flat_map(arpparse::parse_ip_neigh_line)
.collect::<Vec<_>>())
}
});
let res = futures::future::try_join_all(futures).await?;
Ok(res.into_iter().flatten().collect())
}
pub async fn get_macs( pub async fn get_macs(
machine_name: Option<&str>, machine_names: &[impl AsRef<str>],
ips: Option<&[IpAddr]>, ips: &[IpAddr],
dev: Option<&str>, devs: &[impl AsRef<str>],
state: Option<NUDState>, state: &[NUDState],
macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let ip_list: Option<Vec<IpAddr>> = let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect();
ips.map(|slice| slice.iter().copied().map(|ip| ip.to_canonical()).collect()); let ip_m: HashSet<IpAddr> = futures::future::try_join_all(
let ip_list = match (ip_list, machine_name) { machine_names
(Some(list), _) => list, .iter()
(None, Some(name)) => get_ips(name).await?.collect(), .map(|c| async { get_ips(c.as_ref()).await }),
(None, None) => Vec::new(), )
}; .await?
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, dev, state); .into_iter()
if !ip_list.is_empty() { .flatten()
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip))); .collect();
let res = futures::future::try_join_all(futures).await?; let ip_all = if ip_set.is_empty() && ip_m.is_empty() {
Ok(res.into_iter().flatten().collect()) None
} else if ip_set.is_empty() {
Some(ip_m)
} else if ip_m.is_empty() {
Some(ip_set)
} else { } else {
run_one(None).await Some({
} ip_set.retain(|c| ip_m.contains(c)); // inline AHHH
ip_set
})
};
let opt_dev = if devs.len() > 1 {
None
} else {
devs.iter().next().map(AsRef::as_ref)
};
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, opt_dev, state);
let mut ip_filtered = if let Some(something) = ip_all {
if something.len() == 1 {
run_one(something.into_iter().next()).await?
} else {
run_one(None)
.await?
.into_iter()
.filter(|c| something.contains(&c.ip))
.collect()
}
} else {
run_one(None).await?
};
// Apply additional filters if any were provided
if !devs.is_empty() || !macs.is_empty() || !state.is_empty() {
let devset: HashSet<_> = devs.iter().map(AsRef::as_ref).collect();
let macset: HashSet<_> = macs.iter().collect();
ip_filtered.retain(|entry| {
// Dev filter: if we're filtering by dev, entry must have a dev AND it must be in the set
let dev_ok =
devset.is_empty() || entry.dev.as_deref().is_some_and(|d| devset.contains(d));
// MAC filter: if we're filtering by MAC, entry must have a MAC AND it must be in the set
let mac_ok = macset.is_empty() || entry.mac.is_some_and(|m| macset.contains(&m));
dev_ok && mac_ok
})
};
Ok(ip_filtered)
} }
/// the atomic get_macs. handle ONE thing only. /// the atomic get_macs. handle ONE thing only.
pub async fn get_mac( pub async fn get_mac(
ip: Option<IpAddr>, ip: Option<IpAddr>,
dev: Option<&str>, dev: Option<&str>,
state: Option<NUDState>, state: &[NUDState],
) -> error::Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()]; let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = ip { if let Some(ip) = ip {
args.push("to".into()); args.push("to".into());
@@ -112,7 +123,7 @@ pub async fn get_mac(
args.push("dev".into()); args.push("dev".into());
args.push(d.to_string()); args.push(d.to_string());
} }
if let Some(nud) = state { for nud in state {
args.push("nud".into()); args.push("nud".into());
args.push(nud.as_ip_neigh_arg().into()); args.push(nud.as_ip_neigh_arg().into());
} }
@@ -136,7 +147,9 @@ pub async fn get_mac(
Ok(rows) Ok(rows)
} }
/* pub async fn _get_macs_good( /*
/// get macs where you just run ip neigh then rust handles the filtering (faster than get mac)
pub async fn get_macs_rust(
machine_names: Option<&str>, machine_names: Option<&str>,
ips: Option<&[IpAddr]>, ips: Option<&[IpAddr]>,
devs: Option<&[&str]>, devs: Option<&[&str]>,
File renamed without changes.
+44
View File
@@ -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 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) {
return QueryType::Dev(s.to_string());
}
// Default: name last // it will fail also
QueryType::Unknown(s.to_string())
}
-61
View File
@@ -1,9 +1,4 @@
use std::net::IpAddr;
use axum::{http::header, response::IntoResponse}; use axum::{http::header, response::IntoResponse};
use macaddr::MacAddr;
use crate::{arpparse::NUDState, route::DeviceQuery, utils::query::dev::has_dev};
pub async fn serve_js(content: &'static str) -> impl IntoResponse { pub async fn serve_js(content: &'static str) -> impl IntoResponse {
( (
@@ -14,59 +9,3 @@ pub async fn serve_js(content: &'static str) -> impl IntoResponse {
content, content,
) )
} }
pub async fn status_smart_redirect(q: String) -> DeviceQuery {
let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q)
} else {
q.trim()
};
// 1) IP
let ip = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok())
} else {
s.parse::<IpAddr>().ok()
};
if let Some(ip) = ip {
let ip = vec![ip];
return DeviceQuery {
ip,
..Default::default()
};
}
// 2) MAC
if let Ok(mac) = s.parse::<MacAddr>() {
let mac = vec![mac];
return DeviceQuery {
mac,
..Default::default()
};
}
// 3) NUD state (reachable, stale, ...)
if let Ok(state) = s.parse::<NUDState>() {
let nud = vec![state];
return DeviceQuery {
nud,
..Default::default()
};
}
// 4) Known device? prefer dev first
if has_dev(s) {
return DeviceQuery {
dev: vec![s.to_string()],
..Default::default()
};
}
// 5) Try DNS: if it resolves, treat as name
if tokio::net::lookup_host((s, 0)).await.is_ok() {
return DeviceQuery {
name: Some(s.to_string()),
..Default::default()
};
}
// Default: name last // it will fail also
DeviceQuery {
name: Some(s.to_string()),
..Default::default()
}
}
+11 -4
View File
@@ -22,11 +22,19 @@ impl TryFrom<RouteWakeTarget> for WakeTarget {
} }
} }
impl From<WakeTargetResult> for RouteWakeResult { impl From<WakeTarget> for RouteWakeTarget {
fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> Self { fn from(WakeTarget { ip, mac }: WakeTarget) -> Self {
Self { Self {
ip: Some(ip), ip: Some(ip),
mac: Some(mac), mac: Some(mac),
}
}
}
impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTargetResult { target, status }: WakeTargetResult) -> Self {
Self {
target: target.into(),
status: status.into(), status: status.into(),
} }
} }
@@ -35,8 +43,7 @@ impl From<WakeTargetResult> for RouteWakeResult {
impl RouteWakeTarget { impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult { pub fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult { RouteWakeResult {
ip: self.ip, target: self,
mac: self.mac,
status: RouteWakeStatus::Incomplete, status: RouteWakeStatus::Incomplete,
} }
} }
+10 -37
View File
@@ -1,38 +1,10 @@
pub mod impls; pub mod impls;
use std::{io, net::IpAddr}; use std::{io, net::IpAddr};
use futures::TryFutureExt;
use macaddr::MacAddr; use macaddr::MacAddr;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use crate::utils::query::get_macs_2_mac;
pub async fn wake(machine_name: &str) -> io::Result<u32> {
let suh = UdpSocket::bind("0.0.0.0:0").await?;
suh.set_broadcast(true)?;
let /* mut */ macs = get_macs_2_mac(machine_name).await.unwrap_or_default();
// macs.extend(*LDA_MACS_2);
let mut sent_ok = 0;
for mac in macs {
let mb = mac.as_bytes();
let mut pac = [0; 6 + 6 * 16]; // 6x FF + 16x mac6
pac[..6].fill(0xff);
for i in 1..=16 {
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
}
match suh
.send_to(&pac, (IpAddr::from([192, 168, 100, 255]), 9))
.await
{
Ok(n) if n == pac.len() => sent_ok += 1,
Ok(n) => eprintln!("partial send ({n}/{})", pac.len()),
Err(e) => eprintln!("send error: {e}"),
}
}
Ok(sent_ok)
}
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTarget { pub struct WakeTarget {
pub ip: IpAddr, pub ip: IpAddr,
@@ -40,8 +12,7 @@ pub struct WakeTarget {
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub ip: IpAddr, pub target: WakeTarget,
pub mac: MacAddr,
pub status: WakeStatus, pub status: WakeStatus,
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
@@ -55,18 +26,18 @@ impl WakeTarget {
Self { ip, mac } Self { ip, mac }
} }
fn good(self) -> WakeTargetResult { fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::Success) WakeTargetResult::new(self, WakeStatus::Success)
} }
fn bad(self) -> WakeTargetResult { fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize) WakeTargetResult::new(self, WakeStatus::WrongSize)
} }
fn errored(self) -> WakeTargetResult { fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress) WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
} }
} }
impl WakeTargetResult { impl WakeTargetResult {
fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self { fn new(target: WakeTarget, status: WakeStatus) -> Self {
Self { ip, mac, status } Self { target, status }
} }
} }
@@ -74,7 +45,9 @@ impl WakeTargetResult {
pub async fn _wake_multi( pub async fn _wake_multi(
targets: impl IntoIterator<Item = WakeTarget>, targets: impl IntoIterator<Item = WakeTarget>,
) -> io::Result<Vec<WakeTargetResult>> { ) -> io::Result<Vec<WakeTargetResult>> {
let sock = UdpSocket::bind(":0").await?; let sock = UdpSocket::bind("[::]:0")
.or_else(|_| UdpSocket::bind(":0"))
.await?;
sock.set_broadcast(true)?; sock.set_broadcast(true)?;
let fs = targets.into_iter().map(|t| wake_one(&sock, t)); let fs = targets.into_iter().map(|t| wake_one(&sock, t));
Ok(futures::future::join_all(fs).await) Ok(futures::future::join_all(fs).await)
+1 -1
View File
@@ -27,7 +27,7 @@
<div class="row muted" style="gap: 16px"> <div class="row muted" style="gap: 16px">
<span <span
>Uses query >Uses query
<span title="available keys: name, ip, mac, dev, nud" <span title="available keys: name, ips, macs, devs, nuds"
>(?name=...)</span >(?name=...)</span
> >
on this page to view the status.<!-- and header (X-Target-Name) so either extractor on this page to view the status.<!-- and header (X-Target-Name) so either extractor
+14 -4
View File
@@ -1,6 +1,8 @@
import { filter_array } from "./status.js";
export const qs = new URLSearchParams(location.search); export const qs = new URLSearchParams(location.search);
// not real cuh // $ is normally queryselector are we fr
export const $ = (id) => document.getElementById(id); export const $ = (id) => document.getElementById(id);
export const elName = $("name"); export const elName = $("name");
export const elCheck = $("check"); export const elCheck = $("check");
@@ -17,10 +19,18 @@ export function setPill(kind, text) {
pill.textContent = text; pill.textContent = text;
} }
export function setLink(name) { export function setLink(name, clear) {
const url = new URL(location.href); const url = new URL(location.href);
if (name) url.searchParams.set("name", name); if (clear) url.searchParams.forEach((_, k) => url.searchParams.delete(k)); // FUCK
else url.searchParams.delete("name"); /* 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); history.replaceState(null, "", url);
link.href = url.toString(); link.href = url.toString();
} }
+12
View File
@@ -1,5 +1,17 @@
import { elLeases } from "./dom.js"; import { elLeases } from "./dom.js";
/**
*
* @param {{
* expires_epoch: Number
* rank?: Number
* ip: String
* mac: String
* nud_state?: String
* name?: String
* }[]} leases
* @returns
*/
export function renderLeases(leases) { export function renderLeases(leases) {
if (!elLeases) return; if (!elLeases) return;
if (!Array.isArray(leases) || leases.length === 0) { if (!Array.isArray(leases) || leases.length === 0) {
+9 -3
View File
@@ -42,7 +42,7 @@ function pickTarget(value) {
elName.value = v; elName.value = v;
updatePreview(); updatePreview();
saveName(v); saveName(v);
setLink(v); setLink(v, true);
fetchStatus(v); fetchStatus(v);
fetchLeases(); fetchLeases();
} }
@@ -74,10 +74,16 @@ const initial = loadName(qs);
if (initial) { if (initial) {
elName.value = initial; elName.value = initial;
updatePreview(); updatePreview();
setLink(initial); if (qs.has("name")) {
fetchStatus(initial); setLink(initial);
fetchStatus(initial);
} else {
fetchStatus();
}
fetchLeases(); fetchLeases();
} else { } else {
setPill("warn", "unknown"); setPill("warn", "unknown");
updatePreview();
fetchStatus();
fetchLeases(); fetchLeases();
} }
+91 -5
View File
@@ -2,15 +2,22 @@ import { elHtml, elLog, setPill, qs, pill } from "./dom.js";
import { rankState } from "./utils.js"; import { rankState } from "./utils.js";
import { merge_wake_data, translate_wake_message } from "./wake.js"; import { merge_wake_data, translate_wake_message } from "./wake.js";
// IpNeighLine
const status_map = { const status_map = {
ip: "ip", ip: "ip",
mac: "mac", mac: "mac",
state: "state", state: "state",
dev: "interface", dev: "interface",
}; };
export const status_array = Object.keys(status_map);
// Filters
export const filter_array = ["ips", "devs", "nuds", "macs"];
const filter_array = ["ip", "dev", "nud", "mac"]; /**
*
* @param {String} name
* @returns {URL}
*/
function buildStatusUrl(name) { function buildStatusUrl(name) {
const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length); const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length);
if (name && !hasExtraFilters) { if (name && !hasExtraFilters) {
@@ -18,13 +25,32 @@ function buildStatusUrl(name) {
} }
const u = new URL("/api/status", location.origin); const u = new URL("/api/status", location.origin);
if (name) u.searchParams.set("name", name); if (name) u.searchParams.set("name", name);
for (const k of Object.keys(status_map)) { for (const k of filter_array) {
const vals = qs.getAll(k); const vals = qs.getAll(k);
for (const v of vals) u.searchParams.append(k, v); for (const v of vals) u.searchParams.append(k, v);
} }
return u; 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) { export function renderStatus(data) {
const tbl = document.createElement("table"); const tbl = document.createElement("table");
tbl.className = "table"; tbl.className = "table";
@@ -82,12 +108,51 @@ export function renderStatus(data) {
if (r >= 5) setPill("ok", "online"); if (r >= 5) setPill("ok", "online");
else if (r >= 2) setPill("warn", "maybe"); else if (r >= 2) setPill("warn", "maybe");
else setPill("bad", "offline"); else setPill("bad", "offline");
if (data.filters.nud?.length > 0) pill.textContent += " (filtered)"; if (data.filters.nuds?.length > 0) pill.textContent += " (filtered)";
} else { } else {
setPill("warn", "unknown"); 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) { export async function fetchStatus(name, render = true, data_wake) {
setPill("warn", "checking…"); setPill("warn", "checking…");
const u = buildStatusUrl(name); const u = buildStatusUrl(name);
@@ -97,6 +162,7 @@ export async function fetchStatus(name, render = true, data_wake) {
if (!r.ok) { if (!r.ok) {
let msg = String(r.status); let msg = String(r.status);
try { try {
/** @type {{error: string}} */
const err = await r.clone().json(); const err = await r.clone().json();
msg = err.error || JSON.stringify(err); msg = err.error || JSON.stringify(err);
} catch { } catch {
@@ -106,6 +172,26 @@ export async function fetchStatus(name, render = true, data_wake) {
setPill("bad", "error"); setPill("bad", "error");
return; 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(); const data = await r.json();
if (data_wake) { if (data_wake) {
+7
View File
@@ -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;
+31 -3
View File
@@ -1,6 +1,9 @@
import { elHtml, elLog, setPill } from "./dom.js"; import { elHtml, elLog, setPill } from "./dom.js";
import { fetchStatus, renderStatus } from "./status.js"; import { fetchStatus, renderStatus } from "./status.js";
/**
* @param {String} name just plain name
*/
export async function sendWake(name) { export async function sendWake(name) {
const data = await fetchStatus(name, false); const data = await fetchStatus(name, false);
if (!data) return; // can not proceed; theres nothing. if (!data) return; // can not proceed; theres nothing.
@@ -51,8 +54,29 @@ export async function sendWake(name) {
/** /**
* *
* @param {Array} table * @param {{
* @param {Array} wake * 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) { export function merge_wake_data(table, wake) {
if (!wake) return table; if (!wake) return table;
@@ -90,7 +114,11 @@ export function merge_wake_data(table, wake) {
return return_array; return return_array;
} }
/**
*
* @param {"incomplete" | "succeed" | "nonexistent_address" | "wrong_size" | any} wake_msg
* @returns {string}
*/
export function translate_wake_message(wake_msg) { export function translate_wake_message(wake_msg) {
switch (wake_msg) { switch (wake_msg) {
case "incomplete": case "incomplete":