5 Commits
37 changed files with 684 additions and 627 deletions

No files matched your search

+6 -1
View File
@@ -2,4 +2,9 @@
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"
+3 -1
View File
@@ -1,8 +1,10 @@
you should use block code to refence code, like so:
```rust
hello_world!(println);
// indented lines
```
in the chat. Else it fucks up formatting.
We work towards clean, maintainable design. im not the best at this thing.
We work towards clean, maintainable design. im not the best at this thing.
+3 -2
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]]
name = "wakey"
version = "0.1.4"
version = "0.1.6"
dependencies = [
"axum",
"axum-extra",
+9 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "wakey"
version = "0.1.4"
version = "0.1.6"
edition = "2024"
publish = ["gitea"]
@@ -16,7 +16,12 @@ serde_json = "1.0.143"
serde_with = { version = "3.14.0", features = ["json"] }
strum = { version = "0.27.2", features = ["derive", "strum_macros"] }
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"
[profile.release]
@@ -25,4 +30,5 @@ strip = true
[features]
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() {
LOGFILE=/var/log/update_tailscale.log
MAX_RETRIES=10
RETRY_COUNT=0
LOGFILE=/var/log/update_tailscale.log
MAX_RETRIES=10
RETRY_COUNT=0
# shellcheck disable=SC2016
nohup sh -c '
# shellcheck disable=SC2016
sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if fn; then
echo "[update_tailscale] Success at $(date)" >>"$LOGFILE"
+7 -7
View File
@@ -8,14 +8,14 @@ NAME=wakey
BIN=/root/.bin/wakey
start_service() {
procd_open_instance
procd_set_param command "$BIN"
procd_set_param respawn 5 1 0
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
procd_open_instance
procd_set_param command "$BIN"
procd_set_param respawn 5 1 0
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
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)
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
[ -n "$pids" ] || exit 0
@@ -18,7 +18,7 @@ kill -TERM "$pids" 2>/dev/null || true
usleep 250000 # uhhh sleep is stupid
remain=""
for p in $pids; do
kill -0 "$p" 2>/dev/null && remain="$remain $p"
kill -0 "$p" 2>/dev/null && remain="$remain $p"
done
[ -z "$remain" ] || kill -KILL "$remain" 2>/dev/null || true
+8 -8
View File
@@ -3,22 +3,22 @@
set -e
# 0th step from lda is to move the pre assed resolv.conf to the real resolv.conf
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
cp -af /rom/etc/resolv.conf /etc/resolv.conf
cp -af /rom/etc/resolv.conf /etc/resolv.conf
fi
if [ -x "$(which tailscale)" ]; then
tailscale update
exit 0
tailscale update
exit 0
fi
# 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)
[ -z "$LATEST" ] && {
echo "❌ Couldn't find latest ARM binary."
exit 1
echo "❌ Couldn't find latest ARM binary."
exit 1
}
URL="https://pkgs.tailscale.com/stable/${LATEST}"
@@ -26,8 +26,8 @@ URL="https://pkgs.tailscale.com/stable/${LATEST}"
cd /var/tmp
if [ -f "$LATEST" ]; then
echo "file exists"
rm "$LATEST"
echo "file exists"
rm "$LATEST"
fi
echo "Downloading $LATEST..."
+88 -88
View File
@@ -24,113 +24,113 @@ STAGING="$TMPDIR/wakey-rootfs.$$.$ARCH"
log() { echo "[update_wakey] $*"; }
fail() {
echo "[update_wakey] ERROR: $*" >&2
exit 1
echo "[update_wakey] ERROR: $*" >&2
exit 1
}
http_get() {
# http_get <url>
if command -v uclient-fetch >/dev/null 2>&1; then
# uclient-fetch supports -O -, write to stdout
uclient-fetch -O - "$1" 2>/dev/null || return 1
elif command -v wget >/dev/null 2>&1; then
# shellcheck disable=SC2086
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fsSL -k "$1" || return 1
else
curl -fsSL "$1" || return 1
fi
else
return 2
fi
# http_get <url>
if command -v uclient-fetch >/dev/null 2>&1; then
# uclient-fetch supports -O -, write to stdout
uclient-fetch -O - "$1" 2>/dev/null || return 1
elif command -v wget >/dev/null 2>&1; then
# shellcheck disable=SC2086
wget ${WAKEY_INSECURE:+--no-check-certificate} -qO- "$1" || return 1
elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fsSL -k "$1" || return 1
else
curl -fsSL "$1" || return 1
fi
else
return 2
fi
}
fetch() {
# fetch <url> <out>
if command -v uclient-fetch >/dev/null 2>&1; then
uclient-fetch -O "$2" "$1" || return 1
elif command -v wget >/dev/null 2>&1; then
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fSL -k -o "$2" "$1" || return 1
else
curl -fSL -o "$2" "$1" || return 1
fi
else
return 2
fi
# fetch <url> <out>
if command -v uclient-fetch >/dev/null 2>&1; then
uclient-fetch -O "$2" "$1" || return 1
elif command -v wget >/dev/null 2>&1; then
wget ${WAKEY_INSECURE:+--no-check-certificate} -O "$2" "$1" || return 1
elif command -v curl >/dev/null 2>&1; then
if [ -n "${WAKEY_INSECURE:-}" ]; then
curl -fSL -k -o "$2" "$1" || return 1
else
curl -fSL -o "$2" "$1" || return 1
fi
else
return 2
fi
}
latest_asset_url() {
# prints URL to stdout, returns 0 on success (public release only)
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
RESP=$(http_get "$API") || return 1
# Extract the first browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
echo "$RESP" |
grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' |
head -n1 |
cut -d '"' -f 4
# prints URL to stdout, returns 0 on success (public release only)
HOST=$1 OWNER=$2 REPO=$3 ARCH=$4
API="https://$HOST/api/v1/repos/$OWNER/$REPO/releases/latest"
RESP=$(http_get "$API") || return 1
# Extract the first browser_download_url ending with our ARCH .tgz (BusyBox-friendly)
echo "$RESP" |
grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$ARCH"'\.tgz"' |
head -n1 |
cut -d '"' -f 4
}
main() {
[ -f "$FLAG" ] && return
URL="${WAKEY_TGZ_URL:-}"
if [ -z "$URL" ]; then
HOST=${WAKEY_HOST:-git.ldlda.com}
OWNER=${WAKEY_OWNER:-lda}
REPO=${WAKEY_REPO:-wakey}
if [ -n "${WAKEY_VERSION:-}" ]; then
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
else
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"
fi
fi
[ -f "$FLAG" ] && return
URL="${WAKEY_TGZ_URL:-}"
if [ -z "$URL" ]; then
HOST=${WAKEY_HOST:-git.ldlda.com}
OWNER=${WAKEY_OWNER:-lda}
REPO=${WAKEY_REPO:-wakey}
if [ -n "${WAKEY_VERSION:-}" ]; then
FILE="wakey-rootfs-${WAKEY_VERSION}-${ARCH}.tgz"
URL="https://$HOST/$OWNER/$REPO/releases/download/${WAKEY_VERSION}/$FILE"
else
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"
fi
fi
log "fetching $URL"
if ! fetch "$URL" "$TMPFILE"; then
fail "download failed"
fi
log "fetching $URL"
if ! fetch "$URL" "$TMPFILE"; then
fail "download failed"
fi
log "installing"
mkdir -p "$TMPDIR" "$STAGING"
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
log "installing"
mkdir -p "$TMPDIR" "$STAGING"
tar -xz -f "$TMPFILE" -C "$STAGING" || fail "extract failed"
# Ensure execute bits on staged files we know should be executable
for f in \
"$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/wakey" \
"$STAGING/root/.bin/kill_wakey.sh" \
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
done
# Ensure execute bits on staged files we know should be executable
for f in \
"$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/wakey" \
"$STAGING/root/.bin/kill_wakey.sh" \
"$STAGING/root/.bin/remote_deploy_wakey.sh"; do
[ -e "$f" ] && chmod +x "$f" 2>/dev/null || true
done
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
for f in \
"$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/"*.sh; do
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
done
# Normalize line endings for shell scripts (avoid CRLF issues on OpenWrt)
for f in \
"$STAGING/etc/init.d/"* \
"$STAGING/etc/ldlda_help/"*.sh \
"$STAGING/root/.bin/"*.sh; do
[ -f "$f" ] && sed -i 's/\r$//' "$f" 2>/dev/null || true
done
# Copy staged tree into /
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
# Copy staged tree into /
tar -C "$STAGING" -cf - . | tar -C / -xpf - || fail "install copy failed"
rm -f "$TMPFILE"
rm -rf "$STAGING"
rm -f "$TMPFILE"
rm -rf "$STAGING"
if [ -f /etc/init.d/wakey ]; then
/etc/init.d/wakey enable || true
/etc/init.d/wakey restart || /etc/init.d/wakey start || true
fi
touch "$FLAG"
log "done"
if [ -f /etc/init.d/wakey ]; then
/etc/init.d/wakey enable || true
/etc/init.d/wakey restart || /etc/init.d/wakey start || true
fi
touch "$FLAG"
log "done"
}
main "$@"
+1 -1
View File
@@ -52,7 +52,7 @@ class RsAssetModule:
body = "\n".join(files + subs)
return (
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 * "}"
)
+14 -14
View File
@@ -10,32 +10,32 @@ INIT="/etc/init.d/wakey"
KILL="/root/.bin/kill_wakey.sh"
if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
exit 2
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
exit 2
fi
[ -f "$BIN_TMP" ] || {
echo "tmp binary not found: $BIN_TMP" >&2
exit 1
echo "tmp binary not found: $BIN_TMP" >&2
exit 1
}
chmod +x "$BIN_TMP" || true
if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then
"$INIT" stop || true
elif [ -x "$KILL" ]; then
sh "$KILL" || true
fi
if [ -x "$INIT" ]; then
"$INIT" stop || true
elif [ -x "$KILL" ]; then
sh "$KILL" || true
fi
fi
mv -f "$BIN_TMP" "$DEST"
if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then
"$INIT" start || "$INIT" restart || true
else
nohup "$DEST" >/dev/null 2>&1 &
fi
if [ -x "$INIT" ]; then
"$INIT" start || "$INIT" restart || true
else
nohup "$DEST" >/dev/null 2>&1 &
fi
fi
exit 0
File renamed without changes.
+2 -1
View File
@@ -2,7 +2,8 @@
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
/// 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 {
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()),
+12 -8
View File
@@ -1,7 +1,14 @@
use axum::{
Router,
routing::{get, post},
};
//! braindead version v0.1.x
//!
//! # 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;
mod arpparse;
pub mod assets;
@@ -10,18 +17,15 @@ mod route;
mod utils;
use std::io;
const MACHINE_NAME: &str = "lda.lan";
#[cfg(target_os = "linux")]
#[tokio::main]
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()
// .route("/home", get(home))
.route("/", get(home_2))
.merge(home_2_route())
.route("/wake", post(wake_handler))
// .route("/status", get(get_status_2))
.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::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::post;
use axum::{Router, extract::Path, response::Redirect, routing::get};
use axum::{extract::Path, response::Redirect};
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
pub async fn status_smart_redirect(
Path(q): Path<String>,
) -> 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}"))),
Err(e) => Err((
StatusCode::BAD_GATEWAY,
Json(StatusError {
Json(ApiError {
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 {
Redirect::permanent(&format!(
"/api/status?name={name}",
@@ -34,12 +65,14 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
))
}
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))
pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
get_ips(&name).await.map_or_else(
|e| {
ApiError {
error: e.to_string(),
}
.into_response()
},
|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};
// DHCP lease endpoints
@@ -14,19 +18,18 @@ pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl Into
.map(boolish_str)
.unwrap_or(false);
match dhcpparse::read_dhcp_leases_with_names().await {
match read_dhcp_leases_with_names().await {
Ok(leases_with_names) => {
if !include_state {
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()
}
Err(e) => (
StatusCode::BAD_GATEWAY,
Json(StatusError {
Json(ApiError {
error: e.to_string(),
..Default::default()
}),
)
.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 -85
View File
@@ -1,33 +1,21 @@
use crate::{
arpparse::NUDState,
utils::{
parse::{de_many, serialize_macs},
query::get_macs,
},
};
use crate::route::error::ApiError;
use axum::{Json, http::StatusCode, response::IntoResponse};
use axum_extra::extract::Query;
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::collections::HashSet;
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)]
pub struct DeviceQuery {
pub name: Option<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
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>,
#[serde(flatten)]
pub filter: Filters,
}
#[derive(Debug, Default, Clone, Hash, Deserialize)]
@@ -43,82 +31,51 @@ pub struct Status<T> {
pub filters: Filters,
}
#[derive(Debug, Default, Serialize)]
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct Filters {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ip: Vec<IpAddr>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub dev: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub nud: Vec<NUDState>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub ips: Vec<IpAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub devs: Vec<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub nuds: Vec<NUDState>,
#[serde(
skip_serializing_if = "Vec::is_empty",
default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs"
)]
pub mac: Vec<MacAddr>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Default)]
pub struct StatusError {
pub name: Option<String>,
pub error: String,
pub macs: Vec<MacAddr>,
}
pub async fn get_status_json(
Query(DeviceQuery {
name,
ip,
dev,
nud,
mac,
filter:
Filters {
ips,
devs,
nuds,
macs,
},
..
}): Query<DeviceQuery>,
) -> impl IntoResponse {
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> {
if slice.is_empty() {
vec![None]
} else {
slice.iter().cloned().map(Some).collect()
}
}
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();
// can we jus collect the whole table instead of doing this.
for d in &dev_opts {
for n in &nud_opts {
tasks.push(get_macs(
// this i hate sm
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<_>>())
match get_macs(
&name.iter().collect::<Vec<_>>(),
&ips,
&devs.iter().collect::<Vec<_>>(),
&nuds,
&macs,
)
.await
{
Ok(mut table) => {
if !mac.is_empty() {
// mac here
let wanted: HashSet<MacAddr> = mac.into_iter().collect();
table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false));
}
Ok(table) => {
let filters = Filters {
ips,
devs,
nuds,
macs,
};
(
StatusCode::OK,
Json(Status {
@@ -131,8 +88,7 @@ pub async fn get_status_json(
}
Err(error) => (
StatusCode::BAD_GATEWAY,
Json(StatusError {
name,
Json(ApiError {
error: error.to_string(),
}),
)
+13 -20
View File
@@ -23,9 +23,8 @@ pub struct WakeResult {
#[skip_serializing_none]
#[derive(Debug, Serialize, Clone, Copy)]
pub struct WakeTargetResult {
pub ip: Option<IpAddr>,
#[serde(serialize_with = "ser_opm")]
pub mac: Option<MacAddr>,
#[serde(flatten)]
pub target: WakeTarget,
pub status: WakeTargetStatus,
}
@@ -41,11 +40,11 @@ pub enum WakeTargetStatus {
}
#[skip_serializing_none]
#[derive(Debug, Deserialize, Clone, Copy)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct WakeTarget {
#[serde(default)]
pub ip: Option<IpAddr>,
#[serde(default, deserialize_with = "des_opm")]
#[serde(default, serialize_with = "ser_opm", deserialize_with = "des_opm")]
pub mac: Option<MacAddr>,
}
@@ -82,21 +81,15 @@ pub async fn wake_multi_split(
.await?;
sock.set_broadcast(true)?;
Ok(
futures::future::join_all(targets.into_iter().map(async |c| {
if c.is_incomplete() {
c.to_incomplete()
} else {
wake_one(
&sock,
c.try_into().expect("complete struct failed to try_into"), // type state pattern?
)
.await
.into()
}
}))
.await,
)
let iter = targets.into_iter().map(async |c| {
if c.is_incomplete() {
c.to_incomplete()
} else {
let t = c.try_into().expect("complete struct failed to try_into");
wake_one(&sock, t).await.into()
}
});
Ok(futures::future::join_all(iter).await)
}
/* #[derive(Debug, Serialize)]
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,
};
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 {
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 {
match get_macs(None, Some(&[addr.into()]), None, None).await {
match get_mac(Some(addr.into()), None, &[] as &[NUDState]).await {
Err(_) => 0,
Ok(l) => l
.into_iter()
+2 -2
View File
@@ -1,5 +1,6 @@
use crate::arpparse::NUDState;
use crate::dhcpparse::DhcpLeaseLine;
use crate::utils::query::get_macs;
use serde_with::skip_serializing_none;
use std::net::IpAddr;
@@ -14,11 +15,10 @@ pub struct DhcpLeaseOut {
/// Enrich DHCP leases with NUD state and rank using get_macs
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 mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> =
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 {
let state = row.state;
let r = state.rank();
+92 -81
View File
@@ -1,13 +1,14 @@
use macaddr::MacAddr;
use crate::arpparse::{self, IpNeighLine, NUDState};
use crate::utils::{
cmd::exec_command,
error::{self, Error, Result},
error::{self, Result},
};
use macaddr::MacAddr;
use std::collections::HashSet;
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))
.await
.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()))
}
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.filter_map(
|IpNeighLine {
ip,
dev: _,
mac,
state,
}| mac.map(|mac| (ip, mac, state)),
)
.collect())
}
pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> {
Ok(get_macs_1(machine_name)
.await?
.into_iter()
.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())
}
// #[deprecated(
// since = "0.1.5",
// note = "just call once everything with get mac
// and then filter it bro WHY DO YOU EVEN DO TS"
// )]
// good now
//
// Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field.
// This is because missing dev/mac usually means the entry is incomplete/transient.
//
// when there is only one MACs (getmac got some), the result will not have them fields.
// so there are three cases:
//
// 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_name: Option<&str>,
ips: Option<&[IpAddr]>,
dev: Option<&str>,
state: Option<NUDState>,
machine_names: &[impl AsRef<str>],
ips: &[IpAddr],
devs: &[impl AsRef<str>],
state: &[NUDState],
macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> {
let ip_list: Option<Vec<IpAddr>> =
ips.map(|slice| slice.iter().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())
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 {
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.
pub async fn get_mac(
ip: Option<IpAddr>,
dev: Option<&str>,
state: Option<NUDState>,
) -> error::Result<Vec<IpNeighLine>> {
state: &[NUDState],
) -> Result<Vec<IpNeighLine>> {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = ip {
args.push("to".into());
@@ -112,7 +123,7 @@ pub async fn get_mac(
args.push("dev".into());
args.push(d.to_string());
}
if let Some(nud) = state {
for nud in state {
args.push("nud".into());
args.push(nud.as_ip_neigh_arg().into());
}
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 macaddr::MacAddr;
use crate::{arpparse::NUDState, route::DeviceQuery, utils::query::dev::has_dev};
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,
)
}
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 {
fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> Self {
impl From<WakeTarget> for RouteWakeTarget {
fn from(WakeTarget { ip, mac }: WakeTarget) -> Self {
Self {
ip: Some(ip),
mac: Some(mac),
}
}
}
impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTargetResult { target, status }: WakeTargetResult) -> Self {
Self {
target: target.into(),
status: status.into(),
}
}
@@ -35,8 +43,7 @@ impl From<WakeTargetResult> for RouteWakeResult {
impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult {
ip: self.ip,
mac: self.mac,
target: self,
status: RouteWakeStatus::Incomplete,
}
}
+6 -36
View File
@@ -5,35 +5,6 @@ use futures::TryFutureExt;
use macaddr::MacAddr;
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)]
pub struct WakeTarget {
pub ip: IpAddr,
@@ -41,8 +12,7 @@ pub struct WakeTarget {
}
#[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTargetResult {
pub ip: IpAddr,
pub mac: MacAddr,
pub target: WakeTarget,
pub status: WakeStatus,
}
#[derive(Debug, Clone, Copy, Hash)]
@@ -56,18 +26,18 @@ impl WakeTarget {
Self { ip, mac }
}
fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::Success)
WakeTargetResult::new(self, WakeStatus::Success)
}
fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize)
WakeTargetResult::new(self, WakeStatus::WrongSize)
}
fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress)
WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
}
}
impl WakeTargetResult {
fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self {
Self { ip, mac, status }
fn new(target: WakeTarget, status: WakeStatus) -> Self {
Self { target, status }
}
}
+1 -1
View File
@@ -27,7 +27,7 @@
<div class="row muted" style="gap: 16px">
<span
>Uses query
<span title="available keys: name, ip, mac, dev, nud"
<span title="available keys: name, ips, macs, devs, nuds"
>(?name=...)</span
>
on this page to view the status.<!-- and header (X-Target-Name) so either extractor
+12
View File
@@ -1,5 +1,17 @@
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) {
if (!elLeases) return;
if (!Array.isArray(leases) || leases.length === 0) {
+89 -3
View File
@@ -2,6 +2,7 @@ import { elHtml, elLog, setPill, qs, pill } from "./dom.js";
import { rankState } from "./utils.js";
import { merge_wake_data, translate_wake_message } from "./wake.js";
// IpNeighLine
const status_map = {
ip: "ip",
mac: "mac",
@@ -9,8 +10,14 @@ const status_map = {
dev: "interface",
};
export const status_array = Object.keys(status_map);
export const filter_array = ["ip", "dev", "nud", "mac"];
// Filters
export const filter_array = ["ips", "devs", "nuds", "macs"];
/**
*
* @param {String} name
* @returns {URL}
*/
function buildStatusUrl(name) {
const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length);
if (name && !hasExtraFilters) {
@@ -25,6 +32,25 @@ function buildStatusUrl(name) {
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) {
const tbl = document.createElement("table");
tbl.className = "table";
@@ -82,12 +108,51 @@ export function renderStatus(data) {
if (r >= 5) setPill("ok", "online");
else if (r >= 2) setPill("warn", "maybe");
else setPill("bad", "offline");
if (data.filters.nud?.length > 0) pill.textContent += " (filtered)";
if (data.filters.nuds?.length > 0) pill.textContent += " (filtered)";
} else {
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) {
setPill("warn", "checking…");
const u = buildStatusUrl(name);
@@ -97,6 +162,7 @@ export async function fetchStatus(name, render = true, data_wake) {
if (!r.ok) {
let msg = String(r.status);
try {
/** @type {{error: string}} */
const err = await r.clone().json();
msg = err.error || JSON.stringify(err);
} catch {
@@ -106,6 +172,26 @@ export async function fetchStatus(name, render = true, data_wake) {
setPill("bad", "error");
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();
if (data_wake) {
+7
View File
@@ -15,6 +15,13 @@ body {
padding: 0;
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
}
@media screen and (max-width: 768px) {
wrap {
margin-inline: 8px;
}
}
body {
display: flex;
min-height: 100dvh;
+31 -3
View File
@@ -1,6 +1,9 @@
import { elHtml, elLog, setPill } from "./dom.js";
import { fetchStatus, renderStatus } from "./status.js";
/**
* @param {String} name just plain name
*/
export async function sendWake(name) {
const data = await fetchStatus(name, false);
if (!data) return; // can not proceed; theres nothing.
@@ -51,8 +54,29 @@ export async function sendWake(name) {
/**
*
* @param {Array} table
* @param {Array} wake
* @param {{
* 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) {
if (!wake) return table;
@@ -90,7 +114,11 @@ export function merge_wake_data(table, wake) {
return return_array;
}
/**
*
* @param {"incomplete" | "succeed" | "nonexistent_address" | "wrong_size" | any} wake_msg
* @returns {string}
*/
export function translate_wake_message(wake_msg) {
switch (wake_msg) {
case "incomplete":