优化agent,修复已知漏洞
This commit is contained in:
1 parent
36f6e1dcbb
commit
ee06b9cd51
13 files changed
+466
-308
No files matched your search
Generated
+58
@@ -2,6 +2,12 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
@@ -45,6 +51,45 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.10.2"
|
||||
@@ -173,6 +218,12 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
@@ -351,6 +402,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
name = "nodeflare-agent"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -559,6 +611,12 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
||||
@@ -7,6 +7,7 @@ description = "Independent Rust telemetry agent for NodeFlare"
|
||||
repository = "https://github.com/imengying/NodeFlare"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.6", default-features = false, features = ["derive", "std", "help", "usage", "error-context", "suggestions"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.11"
|
||||
|
||||
+133
-60
@@ -4,50 +4,104 @@ set -eu
|
||||
SERVICE_NAME="nodeflare"
|
||||
INSTALL_DIR="/opt/nodeflare"
|
||||
AGENT_FILE="$INSTALL_DIR/agent"
|
||||
CONFIG_FILE="$INSTALL_DIR/agent.env"
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
OPENRC_FILE="/etc/init.d/$SERVICE_NAME"
|
||||
|
||||
usage() {
|
||||
echo "Usage: agent.sh install --server-id ID --token TOKEN --url WORKER_URL [--interval 60] [--collect-interval 5]"
|
||||
echo " agent.sh uninstall"
|
||||
echo " agent.sh status"
|
||||
}
|
||||
|
||||
arg() {
|
||||
key="$1"; shift
|
||||
while [ "$#" -gt 1 ]; do
|
||||
[ "$1" = "$key" ] && { printf '%s' "$2"; return 0; }
|
||||
shift
|
||||
done
|
||||
return 1
|
||||
echo "Usage: agent.sh -e ENDPOINT -t TOKEN -s SERVER_ID [-i 60]"
|
||||
echo " agent.sh --uninstall"
|
||||
echo " agent.sh --status"
|
||||
}
|
||||
|
||||
safe_value() {
|
||||
case "$1" in *[!A-Za-z0-9_./:@-]*|'') return 1 ;; esac
|
||||
}
|
||||
|
||||
detect_init_system() {
|
||||
if [ -f /etc/alpine-release ] && command -v rc-service >/dev/null 2>&1; then
|
||||
printf 'openrc'
|
||||
elif [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then
|
||||
printf 'systemd'
|
||||
elif command -v rc-service >/dev/null 2>&1 && [ -d /etc/init.d ]; then
|
||||
printf 'openrc'
|
||||
else
|
||||
printf 'unknown'
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_curl() {
|
||||
command -v curl >/dev/null 2>&1 && return
|
||||
echo "curl not found; installing it..."
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y curl
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y curl
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y curl
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
apk add --no-cache curl
|
||||
else
|
||||
echo "curl is required and no supported package manager was found" >&2
|
||||
exit 1
|
||||
fi
|
||||
command -v curl >/dev/null 2>&1 || { echo "Failed to install curl" >&2; exit 1; }
|
||||
}
|
||||
|
||||
explain_exec_failure() {
|
||||
target="$1"
|
||||
if [ ! -f "$target" ]; then
|
||||
echo "Downloaded Agent disappeared before validation: $target" >&2
|
||||
elif [ ! -x "$target" ]; then
|
||||
echo "Downloaded Agent is not executable: $target" >&2
|
||||
elif [ "$(dd if="$target" bs=4 count=1 2>/dev/null)" != "$(printf '\177ELF')" ]; then
|
||||
echo "Downloaded asset is not a Linux ELF executable" >&2
|
||||
else
|
||||
echo "Downloaded Agent could not run on this system." >&2
|
||||
echo "Use a release built with the current static Linux targets, then retry installation." >&2
|
||||
fi
|
||||
}
|
||||
|
||||
install_agent() {
|
||||
[ "$(id -u)" -eq 0 ] || { echo "Run install as root" >&2; exit 1; }
|
||||
command -v curl >/dev/null || { echo "curl is required" >&2; exit 1; }
|
||||
ensure_curl
|
||||
command -v sha256sum >/dev/null || { echo "sha256sum is required" >&2; exit 1; }
|
||||
server_id=$(arg --server-id "$@") || { usage; exit 1; }
|
||||
token=$(arg --token "$@") || { usage; exit 1; }
|
||||
worker_url=$(arg --url "$@") || { usage; exit 1; }
|
||||
interval=$(arg --interval "$@" || printf '60')
|
||||
collect_interval=$(arg --collect-interval "$@" || printf '5')
|
||||
worker_url=${worker_url%/}
|
||||
safe_value "$server_id" && safe_value "$token" && safe_value "$worker_url" || { echo "Invalid install argument" >&2; exit 1; }
|
||||
case "$worker_url" in
|
||||
server_id=""
|
||||
token=""
|
||||
endpoint=""
|
||||
interval=60
|
||||
interval_set=false
|
||||
while [ "$#" -gt 0 ]; do
|
||||
option="$1"
|
||||
case "$option" in
|
||||
-s|-t|-e|-i)
|
||||
[ "$#" -ge 2 ] || { echo "Missing value for $option" >&2; usage; exit 1; }
|
||||
value="$2"
|
||||
shift 2
|
||||
;;
|
||||
*) echo "Unknown argument: $option" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
case "$option" in
|
||||
-s) [ -z "$server_id" ] || { echo "Duplicate argument: $option" >&2; exit 1; }; server_id="$value" ;;
|
||||
-t) [ -z "$token" ] || { echo "Duplicate argument: $option" >&2; exit 1; }; token="$value" ;;
|
||||
-e) [ -z "$endpoint" ] || { echo "Duplicate argument: $option" >&2; exit 1; }; endpoint="$value" ;;
|
||||
-i) [ "$interval_set" = false ] || { echo "Duplicate argument: $option" >&2; exit 1; }; interval="$value"; interval_set=true ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$server_id" ] && [ -n "$token" ] && [ -n "$endpoint" ] || { usage; exit 1; }
|
||||
endpoint=${endpoint%/}
|
||||
[ ${#server_id} -le 160 ] && [ ${#token} -le 512 ] && [ ${#endpoint} -le 2048 ] || { echo "Install argument is too long" >&2; exit 1; }
|
||||
safe_value "$server_id" && safe_value "$token" && safe_value "$endpoint" || { echo "Invalid install argument" >&2; exit 1; }
|
||||
case "$endpoint" in
|
||||
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
|
||||
*) echo "Worker URL must use HTTPS; HTTP is only allowed for loopback development" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$worker_url" in *@*) echo "Worker URL must not contain user information" >&2; exit 1 ;; esac
|
||||
case "$endpoint" in *@*) echo "Endpoint must not contain user information" >&2; exit 1 ;; esac
|
||||
case "$interval" in ''|*[!0-9]*) echo "Invalid interval" >&2; exit 1 ;; esac
|
||||
[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || { echo "Interval must be between 15 and 3600 seconds" >&2; exit 1; }
|
||||
case "$collect_interval" in ''|*[!0-9]*) echo "Invalid collect interval" >&2; exit 1 ;; esac
|
||||
[ "$collect_interval" -ge 2 ] && [ "$collect_interval" -le 60 ] && [ "$collect_interval" -le "$interval" ] || { echo "Collect interval must be between 2 and 60 seconds and no greater than report interval" >&2; exit 1; }
|
||||
case "$(uname -m)" in x86_64|amd64) arch="x86_64" ;; aarch64|arm64) arch="aarch64" ;; *) echo "Unsupported architecture" >&2; exit 1 ;; esac
|
||||
init_system=$(detect_init_system)
|
||||
[ "$init_system" != "unknown" ] || { echo "A running systemd or OpenRC installation is required" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
temporary="$INSTALL_DIR/.agent.$$.download"
|
||||
@@ -58,6 +112,11 @@ install_agent() {
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'User-Agent: nodeflare-installer' \
|
||||
"$release_api")
|
||||
release_tag=$(printf '%s\n' "$release_json" | tr ',' '\n' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
|
||||
printf '%s\n' "$release_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' || {
|
||||
echo "GitHub latest release has an invalid tag: ${release_tag:-missing}" >&2
|
||||
exit 1
|
||||
}
|
||||
expected=$(printf '%s\n' "$release_json" | tr '{' '\n' | awk -v name="$artifact" '
|
||||
{
|
||||
compact = $0
|
||||
@@ -77,29 +136,33 @@ install_agent() {
|
||||
}
|
||||
')
|
||||
[ -n "$expected" ] || { echo "GitHub release does not contain a SHA-256 digest for $artifact" >&2; exit 1; }
|
||||
release_base="https://github.com/imengying/NodeFlare/releases/latest/download"
|
||||
release_base="https://github.com/imengying/NodeFlare/releases/download/$release_tag"
|
||||
curl --fail --location --silent --show-error --max-time 120 \
|
||||
"$release_base/$artifact" \
|
||||
-o "$temporary"
|
||||
actual=$(sha256sum "$temporary" | awk '{ print $1 }')
|
||||
[ -n "$expected" ] && [ "$actual" = "$expected" ] || { echo "Agent checksum verification failed" >&2; exit 1; }
|
||||
chmod 755 "$temporary"
|
||||
"$temporary" version >/dev/null
|
||||
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
|
||||
rc-service "$SERVICE_NAME" stop 2>/dev/null || true
|
||||
if ! installed_version=$("$temporary" --version); then
|
||||
explain_exec_failure "$temporary"
|
||||
exit 1
|
||||
fi
|
||||
installed_version=${installed_version##* }
|
||||
printf '%s\n' "$installed_version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || {
|
||||
echo "Downloaded Agent returned an invalid version: $installed_version" >&2
|
||||
exit 1
|
||||
}
|
||||
[ "$installed_version" = "${release_tag#v}" ] || {
|
||||
echo "Release $release_tag contains Agent version $installed_version; refusing to install it" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$init_system" in
|
||||
systemd) systemctl stop "$SERVICE_NAME" 2>/dev/null || true ;;
|
||||
openrc) rc-service "$SERVICE_NAME" stop 2>/dev/null || true ;;
|
||||
esac
|
||||
mv "$temporary" "$AGENT_FILE"
|
||||
trap - EXIT HUP INT TERM
|
||||
{
|
||||
printf 'SERVER_ID=%s\n' "$server_id"
|
||||
printf 'AGENT_TOKEN=%s\n' "$token"
|
||||
printf 'WORKER_URL=%s\n' "$worker_url"
|
||||
printf 'REPORT_INTERVAL=%s\n' "$interval"
|
||||
printf 'COLLECT_INTERVAL=%s\n' "$collect_interval"
|
||||
printf 'NETWORK_INTERFACE=\n'
|
||||
} > "$CONFIG_FILE"
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if [ "$init_system" = "systemd" ]; then
|
||||
printf '%s\n' \
|
||||
'[Unit]' \
|
||||
'Description=NodeFlare Rust Agent' \
|
||||
@@ -108,8 +171,7 @@ install_agent() {
|
||||
'' \
|
||||
'[Service]' \
|
||||
'Type=simple' \
|
||||
"EnvironmentFile=$CONFIG_FILE" \
|
||||
"ExecStart=$AGENT_FILE run" \
|
||||
"ExecStart=$AGENT_FILE -e $endpoint -t $token -s $server_id -i $interval" \
|
||||
'Restart=always' \
|
||||
'RestartSec=10' \
|
||||
'NoNewPrivileges=true' \
|
||||
@@ -122,33 +184,39 @@ install_agent() {
|
||||
'WantedBy=multi-user.target' > "$SERVICE_FILE"
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "$SERVICE_NAME"
|
||||
elif command -v rc-service >/dev/null 2>&1; then
|
||||
systemctl is-active --quiet "$SERVICE_NAME" || {
|
||||
systemctl status "$SERVICE_NAME" --no-pager >&2 || true
|
||||
echo "NodeFlare Agent service failed to start" >&2
|
||||
exit 1
|
||||
}
|
||||
elif [ "$init_system" = "openrc" ]; then
|
||||
printf '%s\n' \
|
||||
'#!/sbin/openrc-run' \
|
||||
"name=\"$SERVICE_NAME\"" \
|
||||
"command=\"$AGENT_FILE\"" \
|
||||
'command_args="run"' \
|
||||
"command_args=\"-e $endpoint -t $token -s $server_id -i $interval\"" \
|
||||
"command_user=\"root\"" \
|
||||
"supervisor=\"supervise-daemon\"" \
|
||||
"respawn_delay=10" \
|
||||
". \"$CONFIG_FILE\"" \
|
||||
'export SERVER_ID AGENT_TOKEN WORKER_URL REPORT_INTERVAL COLLECT_INTERVAL NETWORK_INTERFACE' \
|
||||
'depend() { need net; }' > "$OPENRC_FILE"
|
||||
chmod 755 "$OPENRC_FILE"
|
||||
rc-update add "$SERVICE_NAME" default
|
||||
rc-service "$SERVICE_NAME" restart
|
||||
else
|
||||
echo "systemd or OpenRC is required" >&2
|
||||
exit 1
|
||||
rc-service "$SERVICE_NAME" status >/dev/null || {
|
||||
rc-service "$SERVICE_NAME" status >&2 || true
|
||||
echo "NodeFlare Agent service failed to start" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
echo "NodeFlare Rust agent installed."
|
||||
echo "NodeFlare Agent $installed_version installed with $init_system."
|
||||
}
|
||||
|
||||
status_agent() {
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
init_system=$(detect_init_system)
|
||||
if [ "$init_system" = "systemd" ]; then
|
||||
if systemctl status "$SERVICE_NAME" --no-pager; then return 0; else return $?; fi
|
||||
fi
|
||||
if command -v rc-service >/dev/null 2>&1; then
|
||||
if [ "$init_system" = "openrc" ]; then
|
||||
rc-service "$SERVICE_NAME" status
|
||||
return $?
|
||||
fi
|
||||
@@ -158,19 +226,24 @@ status_agent() {
|
||||
|
||||
uninstall_agent() {
|
||||
[ "$(id -u)" -eq 0 ] || { echo "Run uninstall as root" >&2; exit 1; }
|
||||
systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true
|
||||
rc-service "$SERVICE_NAME" stop 2>/dev/null || true
|
||||
rc-update del "$SERVICE_NAME" default 2>/dev/null || true
|
||||
init_system=$(detect_init_system)
|
||||
case "$init_system" in
|
||||
systemd) systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true ;;
|
||||
openrc)
|
||||
rc-service "$SERVICE_NAME" stop 2>/dev/null || true
|
||||
rc-update del "$SERVICE_NAME" default 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
rm -f "$SERVICE_FILE"
|
||||
rm -f "$OPENRC_FILE"
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
[ "$init_system" != "systemd" ] || systemctl daemon-reload 2>/dev/null || true
|
||||
rm -rf "$INSTALL_DIR"
|
||||
echo "NodeFlare agent removed."
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install) shift; install_agent "$@" ;;
|
||||
uninstall) uninstall_agent ;;
|
||||
status) status_agent ;;
|
||||
*) usage; exit 1 ;;
|
||||
--uninstall) [ "$#" -eq 1 ] || { usage; exit 1; }; uninstall_agent ;;
|
||||
--status) [ "$#" -eq 1 ] || { usage; exit 1; }; status_agent ;;
|
||||
-h|--help|'') usage; exit 0 ;;
|
||||
*) install_agent "$@" ;;
|
||||
esac
|
||||
+54
-36
@@ -6,20 +6,12 @@ INSTALL_DIR="/usr/local/libexec/nodeflare"
|
||||
AGENT_FILE="$INSTALL_DIR/agent"
|
||||
PLIST_FILE="/Library/LaunchDaemons/$LABEL.plist"
|
||||
|
||||
arg() {
|
||||
key="$1"; shift
|
||||
while [ "$#" -gt 1 ]; do
|
||||
[ "$1" = "$key" ] && { printf '%s' "$2"; return 0; }
|
||||
shift
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
safe_value() {
|
||||
case "$1" in *[!A-Za-z0-9_./:@-]*|'') return 1 ;; esac
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "uninstall" ]; then
|
||||
if [ "${1:-}" = "--uninstall" ]; then
|
||||
[ "$#" -eq 1 ] || { echo "--uninstall does not accept additional arguments" >&2; exit 1; }
|
||||
[ "$(id -u)" -eq 0 ] || { echo "Run as root" >&2; exit 1; }
|
||||
launchctl bootout system "$PLIST_FILE" 2>/dev/null || true
|
||||
rm -f "$PLIST_FILE" "$AGENT_FILE"
|
||||
@@ -28,33 +20,54 @@ if [ "${1:-}" = "uninstall" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${1:-}" = "status" ]; then
|
||||
if [ "${1:-}" = "--status" ]; then
|
||||
[ "$#" -eq 1 ] || { echo "--status does not accept additional arguments" >&2; exit 1; }
|
||||
launchctl print "system/$LABEL"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
[ "${1:-}" = "install" ] || { echo "Usage: install-macos.sh <install|uninstall|status> --server-id ID --token TOKEN --url URL" >&2; exit 1; }
|
||||
shift
|
||||
[ "${1:-}" != "-h" ] && [ "${1:-}" != "--help" ] && [ "$#" -gt 0 ] || {
|
||||
echo "Usage: install-macos.sh -e ENDPOINT -t TOKEN -s SERVER_ID [-i 60]"
|
||||
echo " install-macos.sh --uninstall|--status"
|
||||
exit 0
|
||||
}
|
||||
[ "$(id -u)" -eq 0 ] || { echo "Run as root" >&2; exit 1; }
|
||||
[ "$(uname -m)" = "arm64" ] || { echo "Only Apple Silicon (arm64) is supported" >&2; exit 1; }
|
||||
command -v curl >/dev/null || { echo "curl is required" >&2; exit 1; }
|
||||
command -v shasum >/dev/null || { echo "shasum is required" >&2; exit 1; }
|
||||
server_id=$(arg --server-id "$@") || exit 1
|
||||
token=$(arg --token "$@") || exit 1
|
||||
worker_url=$(arg --url "$@") || exit 1
|
||||
interval=$(arg --interval "$@" || printf '60')
|
||||
collect_interval=$(arg --collect-interval "$@" || printf '5')
|
||||
worker_url=${worker_url%/}
|
||||
safe_value "$server_id" && safe_value "$token" && safe_value "$worker_url" || { echo "Invalid argument" >&2; exit 1; }
|
||||
case "$worker_url" in
|
||||
server_id=""
|
||||
token=""
|
||||
endpoint=""
|
||||
interval=60
|
||||
interval_set=false
|
||||
while [ "$#" -gt 0 ]; do
|
||||
option="$1"
|
||||
case "$option" in
|
||||
-s|-t|-e|-i)
|
||||
[ "$#" -ge 2 ] || { echo "Missing value for $option" >&2; exit 1; }
|
||||
value="$2"
|
||||
shift 2
|
||||
;;
|
||||
*) echo "Unknown argument: $option" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$option" in
|
||||
-s) [ -z "$server_id" ] || { echo "Duplicate argument: $option" >&2; exit 1; }; server_id="$value" ;;
|
||||
-t) [ -z "$token" ] || { echo "Duplicate argument: $option" >&2; exit 1; }; token="$value" ;;
|
||||
-e) [ -z "$endpoint" ] || { echo "Duplicate argument: $option" >&2; exit 1; }; endpoint="$value" ;;
|
||||
-i) [ "$interval_set" = false ] || { echo "Duplicate argument: $option" >&2; exit 1; }; interval="$value"; interval_set=true ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$server_id" ] && [ -n "$token" ] && [ -n "$endpoint" ] || { echo "Missing required argument" >&2; exit 1; }
|
||||
endpoint=${endpoint%/}
|
||||
[ ${#server_id} -le 160 ] && [ ${#token} -le 512 ] && [ ${#endpoint} -le 2048 ] || { echo "Install argument is too long" >&2; exit 1; }
|
||||
safe_value "$server_id" && safe_value "$token" && safe_value "$endpoint" || { echo "Invalid argument" >&2; exit 1; }
|
||||
case "$endpoint" in
|
||||
https://?*|http://localhost|http://localhost/*|http://localhost:*|http://127.0.0.1|http://127.0.0.1/*|http://127.0.0.1:*) ;;
|
||||
*) echo "Worker URL must use HTTPS; HTTP is only allowed for loopback development" >&2; exit 1 ;;
|
||||
esac
|
||||
case "$worker_url" in *@*) echo "Worker URL must not contain user information" >&2; exit 1 ;; esac
|
||||
case "$endpoint" in *@*) echo "Endpoint must not contain user information" >&2; exit 1 ;; esac
|
||||
case "$interval" in ''|*[!0-9]*) echo "Invalid interval" >&2; exit 1 ;; esac
|
||||
[ "$interval" -ge 15 ] && [ "$interval" -le 3600 ] || { echo "Interval must be between 15 and 3600 seconds" >&2; exit 1; }
|
||||
case "$collect_interval" in ''|*[!0-9]*) echo "Invalid collect interval" >&2; exit 1 ;; esac
|
||||
[ "$collect_interval" -ge 2 ] && [ "$collect_interval" -le 60 ] && [ "$collect_interval" -le "$interval" ] || { echo "Collect interval must be between 2 and 60 seconds and no greater than report interval" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
temporary="$INSTALL_DIR/.agent.$$.download"
|
||||
@@ -65,6 +78,8 @@ release_json=$(curl --fail --location --silent --show-error --max-time 30 \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'User-Agent: nodeflare-installer' \
|
||||
"$release_api")
|
||||
release_tag=$(printf '%s\n' "$release_json" | tr ',' '\n' | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
|
||||
printf '%s\n' "$release_tag" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' || { echo "GitHub latest release has an invalid tag" >&2; exit 1; }
|
||||
expected=$(printf '%s\n' "$release_json" | tr '{' '\n' | awk -v name="$artifact" '
|
||||
{
|
||||
compact = $0
|
||||
@@ -84,31 +99,25 @@ expected=$(printf '%s\n' "$release_json" | tr '{' '\n' | awk -v name="$artifact"
|
||||
}
|
||||
')
|
||||
[ -n "$expected" ] || { echo "GitHub release does not contain a SHA-256 digest for $artifact" >&2; exit 1; }
|
||||
release_base="https://github.com/imengying/NodeFlare/releases/latest/download"
|
||||
release_base="https://github.com/imengying/NodeFlare/releases/download/$release_tag"
|
||||
curl --fail --location --silent --show-error --max-time 120 \
|
||||
"$release_base/$artifact" \
|
||||
-o "$temporary"
|
||||
actual=$(shasum -a 256 "$temporary" | awk '{ print $1 }')
|
||||
[ -n "$expected" ] && [ "$actual" = "$expected" ] || { echo "Agent checksum verification failed" >&2; exit 1; }
|
||||
chmod 755 "$temporary"
|
||||
"$temporary" version >/dev/null
|
||||
installed_version=$("$temporary" --version) || { echo "Downloaded Agent cannot run on this macOS system" >&2; exit 1; }
|
||||
installed_version=${installed_version##* }
|
||||
[ "$installed_version" = "${release_tag#v}" ] || { echo "Release $release_tag contains Agent version $installed_version" >&2; exit 1; }
|
||||
launchctl bootout system "$PLIST_FILE" 2>/dev/null || true
|
||||
mv "$temporary" "$AGENT_FILE"
|
||||
trap - EXIT HUP INT TERM
|
||||
|
||||
cat > "$PLIST_FILE" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>Label</key><string>$LABEL</string>
|
||||
<key>ProgramArguments</key><array><string>$AGENT_FILE</string><string>run</string></array>
|
||||
<key>EnvironmentVariables</key><dict>
|
||||
<key>SERVER_ID</key><string>$server_id</string>
|
||||
<key>AGENT_TOKEN</key><string>$token</string>
|
||||
<key>WORKER_URL</key><string>$worker_url</string>
|
||||
<key>REPORT_INTERVAL</key><string>$interval</string>
|
||||
<key>COLLECT_INTERVAL</key><string>$collect_interval</string>
|
||||
</dict>
|
||||
<key>ProgramArguments</key><array><string>$AGENT_FILE</string><string>-e</string><string>$endpoint</string><string>-t</string><string>$token</string><string>-s</string><string>$server_id</string><string>-i</string><string>$interval</string></array>
|
||||
<key>KeepAlive</key><true/><key>RunAtLoad</key><true/>
|
||||
<key>StandardOutPath</key><string>/var/log/nodeflare-agent.log</string>
|
||||
<key>StandardErrorPath</key><string>/var/log/nodeflare-agent.log</string>
|
||||
@@ -116,4 +125,13 @@ cat > "$PLIST_FILE" <<EOF
|
||||
EOF
|
||||
chmod 600 "$PLIST_FILE"
|
||||
launchctl bootstrap system "$PLIST_FILE"
|
||||
echo "NodeFlare Agent installed and started."
|
||||
started=false
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||
sleep 1
|
||||
if launchctl print "system/$LABEL" 2>/dev/null | grep -q 'state = running'; then
|
||||
started=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ "$started" = true ] || { echo "NodeFlare Agent failed to start; inspect /var/log/nodeflare-agent.log" >&2; exit 1; }
|
||||
echo "NodeFlare Agent $installed_version installed and started."
|
||||
+42
-28
@@ -1,17 +1,17 @@
|
||||
[CmdletBinding(DefaultParameterSetName = "Install")]
|
||||
param(
|
||||
[Parameter(Position = 0)][ValidateSet("install", "uninstall", "status")][string]$Action = "install",
|
||||
[string]$ServerId,
|
||||
[string]$Token,
|
||||
[string]$Url,
|
||||
[ValidateRange(15, 3600)][int]$Interval = 60,
|
||||
[ValidateRange(2, 60)][int]$CollectInterval = 5
|
||||
[Parameter(ParameterSetName = "Install", Mandatory = $true)][Alias("s")][string]$ServerId,
|
||||
[Parameter(ParameterSetName = "Install", Mandatory = $true)][Alias("t")][string]$Token,
|
||||
[Parameter(ParameterSetName = "Install", Mandatory = $true)][Alias("e")][string]$Endpoint,
|
||||
[Parameter(ParameterSetName = "Install")][Alias("i")][ValidateRange(15, 3600)][int]$Interval = 60,
|
||||
[Parameter(ParameterSetName = "Uninstall", Mandatory = $true)][switch]$Uninstall,
|
||||
[Parameter(ParameterSetName = "Status", Mandatory = $true)][switch]$Status
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$TaskName = "NodeFlare Agent"
|
||||
$InstallDir = Join-Path $env:ProgramData "NodeFlare"
|
||||
$AgentFile = Join-Path $InstallDir "nodeflare-agent.exe"
|
||||
$RunnerFile = Join-Path $InstallDir "run.ps1"
|
||||
|
||||
function Assert-Safe([string]$Name, [string]$Value) {
|
||||
if ([string]::IsNullOrWhiteSpace($Value) -or $Value -notmatch '^[A-Za-z0-9_./:@-]+$') {
|
||||
@@ -19,7 +19,7 @@ function Assert-Safe([string]$Name, [string]$Value) {
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-WorkerUrl([string]$Value) {
|
||||
function Assert-Endpoint([string]$Value) {
|
||||
$Parsed = $null
|
||||
$SecureScheme = $false
|
||||
if ([Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$Parsed)) {
|
||||
@@ -36,11 +36,11 @@ function Assert-WorkerUrl([string]$Value) {
|
||||
-not [string]::IsNullOrEmpty($Parsed.Query) -or
|
||||
-not [string]::IsNullOrEmpty($Parsed.Fragment)
|
||||
) {
|
||||
throw "Url must use HTTPS; HTTP is only allowed for loopback development"
|
||||
throw "Endpoint must use HTTPS; HTTP is only allowed for loopback development"
|
||||
}
|
||||
}
|
||||
|
||||
if ($Action -eq "uninstall") {
|
||||
if ($Uninstall) {
|
||||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $InstallDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
@@ -48,7 +48,7 @@ if ($Action -eq "uninstall") {
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Action -eq "status") {
|
||||
if ($Status) {
|
||||
$Task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
if ($null -eq $Task) {
|
||||
Write-Error "NodeFlare Agent is not installed."
|
||||
@@ -67,9 +67,13 @@ if ($NativeArchitecture -ne "AMD64") {
|
||||
}
|
||||
Assert-Safe "ServerId" $ServerId
|
||||
Assert-Safe "Token" $Token
|
||||
Assert-WorkerUrl $Url
|
||||
$Url = $Url.TrimEnd('/')
|
||||
$CollectInterval = [Math]::Min($CollectInterval, $Interval)
|
||||
Assert-Endpoint $Endpoint
|
||||
$ServerIdLength = $ServerId.Length
|
||||
$TokenLength = $Token.Length
|
||||
if ($ServerIdLength -gt 160 -or $TokenLength -gt 512) {
|
||||
throw "Install argument is too long"
|
||||
}
|
||||
$Endpoint = $Endpoint.TrimEnd('/')
|
||||
|
||||
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
||||
& icacls.exe $InstallDir /inheritance:r /grant:r 'SYSTEM:(OI)(CI)F' 'Administrators:(OI)(CI)F' | Out-Null
|
||||
@@ -79,39 +83,49 @@ $Artifact = "agent-windows-x86_64.exe"
|
||||
try {
|
||||
$Release = Invoke-RestMethod -Uri $ReleaseApi -Headers @{ Accept = "application/vnd.github+json"; "User-Agent" = "nodeflare-installer" } -TimeoutSec 30
|
||||
$ReleaseAsset = $Release.assets | Where-Object { $_.name -eq $Artifact } | Select-Object -First 1
|
||||
if ($Release.tag_name -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+$' -or $null -eq $ReleaseAsset) {
|
||||
throw "GitHub latest release is invalid or does not contain $Artifact"
|
||||
}
|
||||
$DigestMatch = [regex]::Match([string]$ReleaseAsset.digest, '^sha256:([0-9a-fA-F]{64})$')
|
||||
if ($null -eq $ReleaseAsset -or -not $DigestMatch.Success) {
|
||||
if (-not $DigestMatch.Success) {
|
||||
throw "GitHub release does not contain a SHA-256 digest for $Artifact"
|
||||
}
|
||||
$ExpectedChecksum = $DigestMatch.Groups[1].Value
|
||||
Invoke-WebRequest -Uri $ReleaseAsset.browser_download_url -OutFile $Temporary -TimeoutSec 120
|
||||
$DownloadUrl = "https://github.com/imengying/NodeFlare/releases/download/$($Release.tag_name)/$Artifact"
|
||||
Invoke-WebRequest -Uri $DownloadUrl -OutFile $Temporary -TimeoutSec 120
|
||||
$ActualChecksum = (Get-FileHash -LiteralPath $Temporary -Algorithm SHA256).Hash
|
||||
if ($ActualChecksum -ne $ExpectedChecksum) {
|
||||
throw "Agent checksum verification failed"
|
||||
}
|
||||
& $Temporary version | Out-Null
|
||||
$InstalledVersion = (& $Temporary --version | Out-String).Trim()
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Downloaded Agent failed its version check"
|
||||
}
|
||||
$InstalledVersion = ($InstalledVersion -split '\s+')[-1]
|
||||
$ExpectedVersion = $Release.tag_name.Substring(1)
|
||||
if ($InstalledVersion -ne $ExpectedVersion) {
|
||||
throw "Release $($Release.tag_name) contains Agent version $InstalledVersion"
|
||||
}
|
||||
Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
Move-Item -LiteralPath $Temporary -Destination $AgentFile -Force
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $Temporary -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
@"
|
||||
`$env:SERVER_ID='$ServerId'
|
||||
`$env:AGENT_TOKEN='$Token'
|
||||
`$env:WORKER_URL='$Url'
|
||||
`$env:REPORT_INTERVAL='$Interval'
|
||||
`$env:COLLECT_INTERVAL='$CollectInterval'
|
||||
& '$AgentFile' run
|
||||
"@ | Set-Content -LiteralPath $RunnerFile -Encoding UTF8
|
||||
|
||||
$TaskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$RunnerFile`""
|
||||
$TaskArguments = "-e $Endpoint -t $Token -s $ServerId -i $Interval"
|
||||
$TaskAction = New-ScheduledTaskAction -Execute $AgentFile -Argument $TaskArguments
|
||||
$Trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$Settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 3650)
|
||||
$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $TaskAction -Trigger $Trigger -Settings $Settings -Principal $Principal -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
Write-Host "NodeFlare Agent installed and started."
|
||||
$Task = $null
|
||||
for ($Attempt = 0; $Attempt -lt 10; $Attempt++) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$Task = Get-ScheduledTask -TaskName $TaskName
|
||||
if ($Task.State -eq "Running") { break }
|
||||
}
|
||||
if ($Task.State -ne "Running") {
|
||||
throw "NodeFlare Agent scheduled task failed to start (state: $($Task.State))"
|
||||
}
|
||||
Write-Host "NodeFlare Agent $InstalledVersion installed and started."
|
||||
+100
-59
@@ -14,6 +14,7 @@ use std::process::Stdio;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
@@ -44,7 +45,7 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
struct RuntimeConfig {
|
||||
server_id: String,
|
||||
token: String,
|
||||
worker_url: String,
|
||||
endpoint: String,
|
||||
report_interval: u64,
|
||||
collect_interval: u64,
|
||||
network_interface: String,
|
||||
@@ -52,6 +53,29 @@ struct RuntimeConfig {
|
||||
latency_tasks: Vec<LatencyTask>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "nodeflare", version = VERSION, about = "NodeFlare monitoring agent")]
|
||||
struct CliOptions {
|
||||
/// NodeFlare endpoint
|
||||
#[arg(short = 'e', value_name = "URL")]
|
||||
endpoint: String,
|
||||
/// Agent token
|
||||
#[arg(short = 't', value_name = "TOKEN")]
|
||||
token: String,
|
||||
/// Server ID
|
||||
#[arg(short = 's', value_name = "ID")]
|
||||
server_id: String,
|
||||
/// Initial report interval in seconds (15-3600)
|
||||
#[arg(short = 'i', value_name = "SECONDS", default_value_t = 60)]
|
||||
interval: u64,
|
||||
/// Submit one report and exit
|
||||
#[arg(long, conflicts_with = "collect")]
|
||||
once: bool,
|
||||
/// Print one metric sample and exit
|
||||
#[arg(long, conflicts_with = "once")]
|
||||
collect: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LatencyTask {
|
||||
@@ -757,7 +781,7 @@ fn collect(config: &RuntimeConfig, latency_results: Vec<LatencyResult>) -> Repor
|
||||
gpu_usage,
|
||||
gpu_model,
|
||||
agent_version: VERSION.to_string(),
|
||||
message: env::var("AGENT_MESSAGE").unwrap_or_default(),
|
||||
message: String::new(),
|
||||
disk_read_bps: io_after
|
||||
.read_sectors
|
||||
.saturating_sub(io_before.read_sectors)
|
||||
@@ -883,29 +907,19 @@ fn collect(config: &RuntimeConfig, latency_results: Vec<LatencyResult>) -> Repor
|
||||
disk_utilization: 0.0,
|
||||
disks: disk_metrics,
|
||||
gpus,
|
||||
message: env::var("AGENT_MESSAGE").unwrap_or_default(),
|
||||
message: String::new(),
|
||||
latency_results,
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_config() -> Result<RuntimeConfig> {
|
||||
let required = |key: &str| -> Result<String> {
|
||||
env::var(key).map_err(|_| format!("missing environment variable {key}").into())
|
||||
};
|
||||
let interval = env::var("REPORT_INTERVAL")
|
||||
.ok()
|
||||
.and_then(|value| value.parse().ok())
|
||||
.unwrap_or(60)
|
||||
.clamp(15, 3600);
|
||||
let collect_interval = env::var("COLLECT_INTERVAL")
|
||||
.ok()
|
||||
.and_then(|value| value.parse().ok())
|
||||
.unwrap_or(5)
|
||||
.clamp(2, 60)
|
||||
.min(interval);
|
||||
let server_id = required("SERVER_ID")?;
|
||||
let token = required("AGENT_TOKEN")?;
|
||||
let worker_url = required("WORKER_URL")?;
|
||||
fn runtime_config(options: &CliOptions) -> Result<RuntimeConfig> {
|
||||
let interval = options.interval;
|
||||
if !(15..=3600).contains(&interval) {
|
||||
return Err("interval must be between 15 and 3600 seconds".into());
|
||||
}
|
||||
let server_id = options.server_id.clone();
|
||||
let token = options.token.clone();
|
||||
let endpoint = options.endpoint.clone();
|
||||
if server_id.is_empty()
|
||||
|| token.is_empty()
|
||||
|| server_id.len() > 160
|
||||
@@ -913,20 +927,20 @@ fn runtime_config() -> Result<RuntimeConfig> {
|
||||
|| server_id.chars().any(char::is_whitespace)
|
||||
|| token.chars().any(char::is_whitespace)
|
||||
{
|
||||
return Err("SERVER_ID or AGENT_TOKEN is invalid".into());
|
||||
return Err("server ID or token is invalid".into());
|
||||
}
|
||||
if !valid_worker_url(&worker_url) {
|
||||
if !valid_endpoint(&endpoint) {
|
||||
return Err(
|
||||
"WORKER_URL must use HTTPS; HTTP is only allowed for loopback development".into(),
|
||||
"endpoint must use HTTPS; HTTP is only allowed for loopback development".into(),
|
||||
);
|
||||
}
|
||||
Ok(RuntimeConfig {
|
||||
server_id,
|
||||
token,
|
||||
worker_url: worker_url.trim_end_matches('/').to_string(),
|
||||
endpoint: endpoint.trim_end_matches('/').to_string(),
|
||||
report_interval: interval,
|
||||
collect_interval,
|
||||
network_interface: env::var("NETWORK_INTERFACE").unwrap_or_default(),
|
||||
collect_interval: 5,
|
||||
network_interface: String::new(),
|
||||
auto_update: true,
|
||||
latency_tasks: Vec::new(),
|
||||
})
|
||||
@@ -943,10 +957,9 @@ fn submit(
|
||||
samples: reports,
|
||||
};
|
||||
let mut response = agent
|
||||
.post(&format!("{}/api/agent/report", config.worker_url))
|
||||
.post(&format!("{}/api/agent/report", config.endpoint))
|
||||
.header("Authorization", format!("Bearer {}", config.token))
|
||||
.header("User-Agent", format!("nodeflare-agent/{VERSION}"))
|
||||
.header("X-Agent-Config-Schema", "1")
|
||||
.header("X-Agent-Config-Sha256", config_hash)
|
||||
.send_json(batch)?;
|
||||
let next_hash = response
|
||||
@@ -1007,7 +1020,7 @@ fn sanitize_latency_tasks(tasks: &[LatencyTask]) -> Vec<LatencyTask> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn valid_worker_url(value: &str) -> bool {
|
||||
fn valid_endpoint(value: &str) -> bool {
|
||||
let value = value.trim_end_matches('/');
|
||||
if value.is_empty() || value.len() > 2048 || value.chars().any(char::is_whitespace) {
|
||||
return false;
|
||||
@@ -1151,11 +1164,16 @@ fn download_agent(
|
||||
fs::set_permissions(destination, fs::Permissions::from_mode(0o755))?;
|
||||
|
||||
let downloaded_version = Command::new(destination)
|
||||
.arg("version")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string());
|
||||
.and_then(|output| {
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.split_whitespace()
|
||||
.next_back()
|
||||
.map(str::to_string)
|
||||
});
|
||||
let valid = downloaded_version.as_deref().map(normalized_version)
|
||||
== Some(normalized_version(expected_version));
|
||||
if !valid {
|
||||
@@ -1214,7 +1232,7 @@ fn update(agent: &ureq::Agent) -> Result<bool> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
fs::rename(&temporary, ¤t)?;
|
||||
let error = Command::new(¤t).arg("run").exec();
|
||||
let error = Command::new(¤t).args(env::args_os().skip(1)).exec();
|
||||
Err(error.into())
|
||||
}
|
||||
|
||||
@@ -1227,7 +1245,8 @@ fn update(agent: &ureq::Agent) -> Result<bool> {
|
||||
"Move-Item -LiteralPath $env:NODEFLARE_UPDATE_NEW ",
|
||||
"-Destination $env:NODEFLARE_UPDATE_CURRENT -Force; ",
|
||||
"try { Start-ScheduledTask -TaskName 'NodeFlare Agent' -ErrorAction Stop } ",
|
||||
"catch { Start-Process -FilePath $env:NODEFLARE_UPDATE_CURRENT -ArgumentList 'run' }"
|
||||
"catch { $restartArgs=@($env:NODEFLARE_UPDATE_ARGS | ConvertFrom-Json); ",
|
||||
"Start-Process -FilePath $env:NODEFLARE_UPDATE_CURRENT -ArgumentList $restartArgs }"
|
||||
);
|
||||
Command::new("powershell.exe")
|
||||
.args([
|
||||
@@ -1240,6 +1259,10 @@ fn update(agent: &ureq::Agent) -> Result<bool> {
|
||||
.env("NODEFLARE_UPDATE_PID", std::process::id().to_string())
|
||||
.env("NODEFLARE_UPDATE_NEW", &temporary)
|
||||
.env("NODEFLARE_UPDATE_CURRENT", ¤t)
|
||||
.env(
|
||||
"NODEFLARE_UPDATE_ARGS",
|
||||
serde_json::to_string(&env::args().skip(1).collect::<Vec<_>>())?,
|
||||
)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
@@ -1248,8 +1271,8 @@ fn update(agent: &ureq::Agent) -> Result<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
fn run(once: bool, print_only: bool) -> Result<()> {
|
||||
let mut config = runtime_config()?;
|
||||
fn run(options: &CliOptions, once: bool, print_only: bool) -> Result<()> {
|
||||
let mut config = runtime_config(options)?;
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.timeout_global(Some(Duration::from_secs(20)))
|
||||
.build()
|
||||
@@ -1360,16 +1383,8 @@ fn run(once: bool, print_only: bool) -> Result<()> {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let result = match env::args().nth(1).as_deref() {
|
||||
Some("run") => run(false, false),
|
||||
Some("once") => run(true, false),
|
||||
Some("collect") => run(true, true),
|
||||
Some("version") | Some("--version") => {
|
||||
println!("{VERSION}");
|
||||
Ok(())
|
||||
}
|
||||
_ => Err("usage: nodeflare-agent <run|once|collect|version>".into()),
|
||||
};
|
||||
let options = CliOptions::parse();
|
||||
let result = run(&options, options.once || options.collect, options.collect);
|
||||
if let Err(error) = result {
|
||||
eprintln!("nodeflare-agent: {error}");
|
||||
std::process::exit(1);
|
||||
@@ -1378,6 +1393,7 @@ fn main() {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::Parser;
|
||||
use std::net::TcpListener;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
@@ -1386,7 +1402,7 @@ mod tests {
|
||||
agent_artifact_name, executable_format_valid, is_public_probe_ip, median,
|
||||
normalized_version, parse_probe_target, ping_latency, prune_report_samples,
|
||||
release_asset_sha256, report_retry_delay, sanitize_latency_tasks, selected_interface,
|
||||
tcp_latency_probe_address, valid_worker_url, version_triplet, GithubReleaseAsset,
|
||||
tcp_latency_probe_address, valid_endpoint, version_triplet, CliOptions, GithubReleaseAsset,
|
||||
LatencyTask, Report, MAX_LATENCY_TASKS, PROBE_ATTEMPTS,
|
||||
};
|
||||
|
||||
@@ -1440,18 +1456,43 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_worker_urls() {
|
||||
assert!(valid_worker_url("https://monitor.example.com/"));
|
||||
assert!(valid_worker_url("http://127.0.0.1:8787"));
|
||||
assert!(valid_worker_url("http://localhost:8787"));
|
||||
assert!(valid_worker_url("http://[::1]:8787"));
|
||||
assert!(!valid_worker_url("http://monitor.example.com"));
|
||||
assert!(!valid_worker_url("http://127.0.0.2:8787"));
|
||||
assert!(!valid_worker_url("monitor.example.com"));
|
||||
assert!(!valid_worker_url("https://"));
|
||||
assert!(!valid_worker_url("https://user@example.com"));
|
||||
assert!(!valid_worker_url("https://monitor.example.com/?token=abc"));
|
||||
assert!(!valid_worker_url("https://bad host.example"));
|
||||
fn validates_endpoints() {
|
||||
assert!(valid_endpoint("https://monitor.example.com/"));
|
||||
assert!(valid_endpoint("http://127.0.0.1:8787"));
|
||||
assert!(valid_endpoint("http://localhost:8787"));
|
||||
assert!(valid_endpoint("http://[::1]:8787"));
|
||||
assert!(!valid_endpoint("http://monitor.example.com"));
|
||||
assert!(!valid_endpoint("http://127.0.0.2:8787"));
|
||||
assert!(!valid_endpoint("monitor.example.com"));
|
||||
assert!(!valid_endpoint("https://"));
|
||||
assert!(!valid_endpoint("https://user@example.com"));
|
||||
assert!(!valid_endpoint("https://monitor.example.com/?token=abc"));
|
||||
assert!(!valid_endpoint("https://bad host.example"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_cli_options() {
|
||||
let parsed = CliOptions::try_parse_from([
|
||||
"nodeflare",
|
||||
"-e",
|
||||
"https://monitor.example.com",
|
||||
"-t",
|
||||
"secret",
|
||||
"-s",
|
||||
"server-a",
|
||||
"-i",
|
||||
"60",
|
||||
"--once",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(parsed.endpoint, "https://monitor.example.com");
|
||||
assert_eq!(parsed.token, "secret");
|
||||
assert_eq!(parsed.server_id, "server-a");
|
||||
assert_eq!(parsed.interval, 60);
|
||||
assert!(parsed.once);
|
||||
assert!(CliOptions::try_parse_from(["nodeflare", "-t"]).is_err());
|
||||
assert!(CliOptions::try_parse_from(["nodeflare", "-t", "first", "-t", "second"]).is_err());
|
||||
assert!(CliOptions::try_parse_from(["nodeflare", "--once", "--collect"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user