7 Commits
41 changed files with 685 additions and 694 deletions

No files matched your search

-5
View File
@@ -3,8 +3,3 @@ 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"
File renamed without changes.
+2 -7
View File
@@ -1,10 +1,5 @@
you should use block code to refence code, like so: you can use block code as 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.
+1 -2
View File
@@ -1,4 +1,3 @@
{ {
"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.6" version = "0.1.2"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
+3 -9
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "wakey" name = "wakey"
version = "0.1.6" version = "0.1.2"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
@@ -16,12 +16,7 @@ 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 = [ tokio = { version = "1.47.1", features = ["fs", "process", "rt-multi-thread", "io-util"] }
"fs",
"process",
"rt-multi-thread",
"io-util",
] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
[profile.release] [profile.release]
@@ -30,5 +25,4 @@ strip = true
[features] [features]
default = ["very-smart-parsing"] default = ["very-smart-parsing"]
very-smart-parsing = [ very-smart-parsing = [] # this is the a-bit-redundant parse thing that copilot made
] # this is the a-bit-redundant parse thing that copilot made
+60
View File
@@ -0,0 +1,60 @@
# taking back my code AHHHH
this is going to be lengthy. ci is spotless but idk how it works cuz powershell. this is good but i cant afford VPN on the router; we will need to do some bullshit.
## overview
we need a:
- small ahh bin in the router.
- forwards a shell and some functions.
- may be shell calling fns maybe some unsafe c. i cant unsafe c.
- big ahh bin in the vps.
- connect to the small ahh bin thru the wide ass internet.
- forwards some APIs.
- static site
- now need no hosts; apis can be called anywhere anyway.
- do offer these in the big ahh bins tho lets not miss out some shit.
- automation
- gitea
- on the vps or the uh idk.
- ABSOLUTELY can auto build main:latest every day / other day.
- and releases.
- router
- a script to pull the tar. from actions artifacts or from releases. (lua or shell yo)
- a script for the bin and the init.d scripts for these two.
- vps
- a systemd service (ima use ai for ts ngl)
- an auto updater. maybe we overkill and dnf. maybe go easy and git clone + cargo install. maybe we pull from release or nightly. idk
i think idk
## router
### current
everything.
we have some functions that call ip neigh a bajillion times.
we have some other functions that read the same file/folder in /sys or sum shit.
we have a function that broadcast to a set ahh address.
we have some routes; and some data structures acting as schema for them ahh. we dont do posts with json but gets with query strings for some ahh reasons.
these are carryover from the early days.
also from the early days are the js.
static was 100% ai with 0 intervention.
we have a search bar of sort that hooks through a smart endpoint that searches for everything.
we have some bullshit. and two buttons to check and wake. that pings `/api/smart/{query}` and `/wake?name=query`. why.
we have the result of the wake and or status and the errors in the first text box.
the list of cliets with names connected to the router the second box.
thats cool.
### changes?
idk changes.
+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
sh -c ' nohup 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
sh -c ' nohup 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.
+1 -2
View File
@@ -2,8 +2,7 @@
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
pub(crate) async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> 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()),
+8 -12
View File
@@ -1,14 +1,7 @@
//! braindead version v0.1.x use axum::{
//! Router,
//! # whats next routing::{get, post},
//! };
//! 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;
@@ -17,15 +10,18 @@ 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}; use crate::route::{api_router, home_2, home_2_route, wake_handler};
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
@@ -0,0 +1,99 @@
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)))
}
+19 -52
View File
@@ -1,63 +1,32 @@
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::routing::post;
use axum::{Router, extract::Path, response::Redirect, routing::get};
use crate::route::status::{DeviceQuery, Filters, NamePath}; use crate::utils::route;
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 match serde_html_form::to_string(route::status_smart_redirect(q).await) {
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(ApiError { Json(StatusError {
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}",
@@ -65,14 +34,12 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
)) ))
} }
pub async fn ip(Path(name): Path<String>) -> impl IntoResponse { pub fn api_router() -> Router {
get_ips(&name).await.map_or_else( Router::new()
|e| { .route("/status/{name}", get(status_redirect))
ApiError { .route("/status", get(get_status_json))
error: e.to_string(), .route("/dhcp_leases", get(get_dhcp_leases))
} .route("/smart/{q}", get(status_smart_redirect))
.into_response() .route("/devs", get(devs_router))
}, .route("/wake", post(wake_multi))
|ips| Json(ips.collect::<Vec<_>>()).into_response(),
)
} }
+5 -8
View File
@@ -1,8 +1,4 @@
use crate::{ use crate::{dhcpparse, route::api::StatusError, 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
@@ -18,18 +14,19 @@ 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 read_dhcp_leases_with_names().await { match dhcpparse::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 = enrich_leases_with_nud_state(leases_with_names).await; let out = crate::utils::query::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(ApiError { Json(StatusError {
error: e.to_string(), error: e.to_string(),
..Default::default()
}), }),
) )
.into_response(), .into_response(),
-17
View File
@@ -1,17 +0,0 @@
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
@@ -1,72 +0,0 @@
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(),
}
}),
)
}
+79 -41
View File
@@ -1,21 +1,30 @@
use crate::route::error::ApiError; use crate::{
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(flatten)] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub filter: Filters, pub ip: Vec<IpAddr>,
#[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)]
@@ -31,51 +40,79 @@ pub struct Status<T> {
pub filters: Filters, pub filters: Filters,
} }
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)] #[derive(Debug, Default, Serialize)]
pub struct Filters { pub struct Filters {
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub ips: Vec<IpAddr>, pub ip: Vec<IpAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub devs: Vec<String>, pub dev: Vec<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub nuds: Vec<NUDState>, pub nud: Vec<NUDState>,
#[serde( #[serde(
default, skip_serializing_if = "Vec::is_empty",
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs" serialize_with = "serialize_macs"
)] )]
pub macs: Vec<MacAddr>, pub mac: 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,
filter: ip,
Filters { dev,
ips, nud,
devs, mac,
nuds,
macs,
},
.. ..
}): Query<DeviceQuery>, }): Query<DeviceQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match get_macs( fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> {
&name.iter().collect::<Vec<_>>(), if slice.is_empty() {
&ips, vec![None]
&devs.iter().collect::<Vec<_>>(), } else {
&nuds, slice.iter().cloned().map(Some).collect()
&macs, }
) }
.await let ips_opt = if ip.is_empty() {
None
} else {
Some(ip.clone())
};
let dev_opts: Vec<Option<String>> = to_opts(&dev);
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
let filters = Filters {
ip,
dev,
nud,
mac: mac.clone(),
};
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(table) => { Ok(mut table) => {
let filters = Filters { if !mac.is_empty() {
ips, let wanted: HashSet<MacAddr> = mac.into_iter().collect();
devs, table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false));
nuds, }
macs,
};
( (
StatusCode::OK, StatusCode::OK,
Json(Status { Json(Status {
@@ -88,7 +125,8 @@ pub async fn get_status_json(
} }
Err(error) => ( Err(error) => (
StatusCode::BAD_GATEWAY, StatusCode::BAD_GATEWAY,
Json(ApiError { Json(StatusError {
name,
error: error.to_string(), error: error.to_string(),
}), }),
) )
+20 -13
View File
@@ -23,8 +23,9 @@ 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 {
#[serde(flatten)] pub ip: Option<IpAddr>,
pub target: WakeTarget, #[serde(serialize_with = "ser_opm")]
pub mac: Option<MacAddr>,
pub status: WakeTargetStatus, pub status: WakeTargetStatus,
} }
@@ -40,11 +41,11 @@ pub enum WakeTargetStatus {
} }
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[derive(Debug, Deserialize, Clone, Copy)]
pub struct WakeTarget { pub struct WakeTarget {
#[serde(default)] #[serde(default)]
pub ip: Option<IpAddr>, pub ip: Option<IpAddr>,
#[serde(default, serialize_with = "ser_opm", deserialize_with = "des_opm")] #[serde(default, deserialize_with = "des_opm")]
pub mac: Option<MacAddr>, pub mac: Option<MacAddr>,
} }
@@ -81,15 +82,21 @@ pub async fn wake_multi_split(
.await?; .await?;
sock.set_broadcast(true)?; sock.set_broadcast(true)?;
let iter = targets.into_iter().map(async |c| { Ok(
if c.is_incomplete() { futures::future::join_all(targets.into_iter().map(async |c| {
c.to_incomplete() if c.is_incomplete() {
} else { c.to_incomplete()
let t = c.try_into().expect("complete struct failed to try_into"); } else {
wake_one(&sock, t).await.into() wake_one(
} &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
@@ -0,0 +1,64 @@
// 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
@@ -1,23 +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;
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_mac}; use crate::{arpparse::NUDState, utils::query::get_macs};
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_mac(Some(addr.into()), None, &[] as &[NUDState]).await { match get_macs(None, Some(&[addr.into()]), None, None).await {
Err(_) => 0, Err(_) => 0,
Ok(l) => l Ok(l) => l
.into_iter() .into_iter()
File renamed without changes.
-1
View File
@@ -7,7 +7,6 @@ pub fn get_dev() -> HashSet<String> {
if e.file_type() if e.file_type()
.map(|ft| { .map(|ft| {
if ft.is_symlink() { if ft.is_symlink() {
// true
fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false) fs::metadata(e.path()).map(|m| m.is_dir()).unwrap_or(false)
} else { } else {
ft.is_dir() ft.is_dir()
+2 -2
View File
@@ -1,6 +1,5 @@
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;
@@ -15,10 +14,11 @@ 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(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await { if let Ok(rows) = get_macs(None, Some(&ips), None, None).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();
+81 -94
View File
@@ -1,14 +1,13 @@
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, Result}, error::{self, Error, 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) -> Result<impl Iterator<Item = IpAddr>> { pub async fn get_ips(machine_name: &str) -> error::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 {
@@ -18,102 +17,92 @@ pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>>
.map(|c| c.ip())) .map(|c| c.ip()))
} }
// #[deprecated( pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
// since = "0.1.5", Ok(get_macs_1(machine_name)
// note = "just call once everything with get mac .await?
// and then filter it bro WHY DO YOU EVEN DO TS" .into_iter()
// )] .filter_map(
// good now |IpNeighLine {
// ip,
// Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field. dev: _,
// This is because missing dev/mac usually means the entry is incomplete/transient. mac,
// state,
// when there is only one MACs (getmac got some), the result will not have them fields. }| mac.map(|mac| (ip, mac, state)),
// 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,
// devset.len() != 1 returns false, but then it works????
// OH THIS fuckass code i added it in the get_mac
// 3. devset.len() > 1. if none then absolutely not match,
// if some then check with the set; thats normal
pub async fn get_macs(
machine_names: &[impl AsRef<str>],
ips: &[IpAddr],
devs: &[impl AsRef<str>],
state: &[NUDState],
macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> {
let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect();
let ip_m: HashSet<IpAddr> = futures::future::try_join_all(
machine_names
.iter()
.map(|c| async { get_ips(c.as_ref()).await }),
)
.await?
.into_iter()
.flatten()
.collect();
let ip_all = if ip_set.is_empty() && ip_m.is_empty() {
None
} else if ip_set.is_empty() {
Some(ip_m)
} else if ip_m.is_empty() {
Some(ip_set)
} else {
Some({
ip_set.retain(|c| ip_m.contains(c)); // inline AHHH
ip_set
})
};
let opt_dev = if devs.len() > 1 { pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> {
None Ok(get_macs_1(machine_name)
} else { .await?
devs.iter().next().map(AsRef::as_ref) .into_iter()
}; .filter_map(
|IpNeighLine {
ip: _,
dev: _,
mac,
state: _,
}| mac,
)
.collect())
}
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, opt_dev, state); pub async fn get_macs_1(machine_name: &str) -> Result<Vec<arpparse::IpNeighLine>> {
let dev = "br-lan";
let mut ip_filtered = if let Some(something) = ip_all { let ips = get_ips(machine_name).await?;
if something.len() == 1 { let futures = ips.map(|ip| {
run_one(something.into_iter().next()).await? let ip = ip.to_canonical();
} else { async move {
run_one(None) let cmd = "ip";
.await? let args = ["neigh", "show", "to", &ip.to_string(), "dev", dev];
.into_iter() let o = exec_command(cmd, args).await?;
.filter(|c| something.contains(&c.ip)) if !o.status.success() {
.collect() 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(
machine_name: Option<&str>,
ips: Option<&[IpAddr]>,
dev: Option<&str>,
state: Option<NUDState>,
) -> Result<Vec<IpNeighLine>> {
let ip_list: Option<Vec<IpAddr>> =
ips.map(|slice| slice.iter().copied().map(|ip| ip.to_canonical()).collect());
let ip_list = match (ip_list, machine_name) {
(Some(list), _) => list,
(None, Some(name)) => get_ips(name).await?.collect(),
(None, None) => Vec::new(),
};
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, dev, state);
if !ip_list.is_empty() {
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip)));
let res = futures::future::try_join_all(futures).await?;
Ok(res.into_iter().flatten().collect())
} else { } else {
run_one(None).await? 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: &[NUDState], state: Option<NUDState>,
) -> Result<Vec<IpNeighLine>> { ) -> error::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());
@@ -123,7 +112,7 @@ pub async fn get_mac(
args.push("dev".into()); args.push("dev".into());
args.push(d.to_string()); args.push(d.to_string());
} }
for nud in state { if let Some(nud) = 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());
} }
@@ -147,9 +136,7 @@ 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]>,
-44
View File
@@ -1,44 +0,0 @@
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,4 +1,9 @@
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 {
( (
@@ -9,3 +14,59 @@ 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()
}
}
+36 -6
View File
@@ -5,6 +5,35 @@ 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,
@@ -12,7 +41,8 @@ pub struct WakeTarget {
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub target: WakeTarget, pub ip: IpAddr,
pub mac: MacAddr,
pub status: WakeStatus, pub status: WakeStatus,
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
@@ -26,18 +56,18 @@ impl WakeTarget {
Self { ip, mac } Self { ip, mac }
} }
fn good(self) -> WakeTargetResult { fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::Success) WakeTargetResult::new(self.ip, self.mac, WakeStatus::Success)
} }
fn bad(self) -> WakeTargetResult { fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::WrongSize) WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize)
} }
fn errored(self) -> WakeTargetResult { fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self, WakeStatus::NonexistentAddress) WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress)
} }
} }
impl WakeTargetResult { impl WakeTargetResult {
fn new(target: WakeTarget, status: WakeStatus) -> Self { fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self {
Self { target, status } Self { ip, mac, status }
} }
} }
+4 -11
View File
@@ -22,19 +22,11 @@ impl TryFrom<RouteWakeTarget> for WakeTarget {
} }
} }
impl From<WakeTarget> for RouteWakeTarget { impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTarget { ip, mac }: WakeTarget) -> Self { fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> 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(),
} }
} }
@@ -43,7 +35,8 @@ impl From<WakeTargetResult> for RouteWakeResult {
impl RouteWakeTarget { impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult { pub fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult { RouteWakeResult {
target: self, ip: self.ip,
mac: self.mac,
status: RouteWakeStatus::Incomplete, status: RouteWakeStatus::Incomplete,
} }
} }
+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, ips, macs, devs, nuds" <span title="available keys: name, ip, mac, dev, nud"
>(?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
-12
View File
@@ -1,17 +1,5 @@
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) {
+3 -89
View File
@@ -2,7 +2,6 @@ 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",
@@ -10,14 +9,8 @@ const status_map = {
dev: "interface", dev: "interface",
}; };
export const status_array = Object.keys(status_map); export const status_array = Object.keys(status_map);
// Filters export const filter_array = ["ip", "dev", "nud", "mac"];
export const filter_array = ["ips", "devs", "nuds", "macs"];
/**
*
* @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) {
@@ -32,25 +25,6 @@ function buildStatusUrl(name) {
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";
@@ -108,51 +82,12 @@ 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.nuds?.length > 0) pill.textContent += " (filtered)"; if (data.filters.nud?.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);
@@ -162,7 +97,6 @@ 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 {
@@ -172,26 +106,6 @@ 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,13 +15,6 @@ 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;
+3 -31
View File
@@ -1,9 +1,6 @@
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.
@@ -54,29 +51,8 @@ export async function sendWake(name) {
/** /**
* *
* @param {{ * @param {Array} table
* ip: String * @param {Array} wake
* 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;
@@ -114,11 +90,7 @@ 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":