update 2026-10-29 01:36:37

This commit is contained in:
action committed 2026-10-29 01:36:37 +08:00
1 parent 3a91d7a1df
commit 36d6943a0b
287 files changed
+39005 -11853

No files matched your search

-750
View File
@@ -1,750 +0,0 @@
#!/bin/sh
. /usr/share/libubox/jshn.sh
. /lib/functions.sh
. /usr/share/honk/lock.sh
. /usr/share/honk/lifecycle.sh
. /usr/share/honk/log-lines.sh
CONTROL=/usr/share/honk/control.sh
CORE=/usr/bin/honk-core
JOBS=/tmp/honk-jobs
JOB_RUNNER=/usr/share/honk/job.sh
UPDATE=/usr/share/honk/update.sh
MAINT=/usr/share/honk/maintenance.sh
REDACT=/usr/share/honk/redact-log.sh
LOG_LEVEL_FILTER=/usr/share/honk/log-level-filter.sh
RESTORE_ROOT=/tmp/honk-maintenance
RESTORE_PATH=$RESTORE_ROOT/restore.tar.gz
emit_result() {
ok=$1
message=$2
[ "$ok" = true ] && ok=1 || ok=0
json_init
json_add_boolean ok "$ok"
json_add_string message "$message"
json_dump
}
emit_errors() {
ok=$1
[ "$ok" = true ] && ok=1 || ok=0
json_init
json_add_boolean ok "$ok"
json_add_array errors
for error in $2; do
json_add_string "" "$error"
done
json_close_array
json_dump
}
lock_owner_invalid() {
[ -d "$HONK_LOCK_DIR" ] || return 1
lock_owner=$(cat "$HONK_LOCK_DIR/pid" 2>/dev/null)
case "$lock_owner" in ''|*[!0-9]*) return 0 ;; esac
return 1
}
status() {
local enabled initialized boot_enabled running api_url api_ready core_version doona_version kernel lan_network listen_port errors listen clash_api_enabled clash_api_port clash_api_secret clash_api_url clash_api_ready clash_ui_ready clash_secret_set
config_load honk
config_get_bool enabled main enabled 0
config_get_bool initialized main initialized 0
config_get_bool boot_enabled main boot_enabled 0
config_get listen_port main listen_port 9527
config_get lan_network main lan_network lan
config_get_bool clash_api_enabled main clash_api_enabled 0
config_get clash_api_port main clash_api_port 9090
config_get clash_api_secret main clash_api_secret ''
managed_pid=$(procd_core_pid)
if is_managed_core "$managed_pid"; then running=1; else running=0; fi
listen=$(sed -n "s/^[[:space:]]*listen:[[:space:]]*'\\([^']*\\)'.*/\\1/p" /etc/honk/system.dae | head -n 1)
case "$listen" in
''|*[!0-9.:]*) listen="127.0.0.1:$listen_port" ;;
esac
api_url="http://$listen"
case "$clash_api_port" in
''|*[!0-9]*) clash_api_port=0 ;;
esac
clash_api_url="http://${listen%:*}:$clash_api_port"
clash_api_ready=0
if [ "$running" = 1 ] && [ "$clash_api_enabled" = 1 ] && [ "$clash_api_port" -gt 0 ] && honk_lifecycle_listener_owned "$managed_pid" "$clash_api_port"; then clash_api_ready=1; fi
clash_ui_ready=0
[ -s /usr/share/honk/zashboard/index.html ] && clash_ui_ready=1
clash_secret_set=0
[ -n "$clash_api_secret" ] && clash_secret_set=1
if [ "$running" = 1 ] && honk_lifecycle_api_owned "$managed_pid"; then api_ready=1; else api_ready=0; fi
core_version=$($CORE --version 2>/dev/null | sed -n '1p')
[ -n "$core_version" ] || core_version=unknown
doona_version=$(sed -n 's/^Version: //p' /usr/share/doona/SOURCE 2>/dev/null | head -n 1)
[ -n "$doona_version" ] || doona_version=unknown
kernel=$(uname -r)
errors=""
if [ "$enabled" != 1 ]; then errors="$errors disabled"; fi
if [ "$initialized" != 1 ]; then errors="$errors not_initialized"; fi
if [ "$enabled" = 1 ] && [ "$initialized" = 1 ] && [ "$running" != 1 ]; then errors="$errors service_not_running"; fi
if [ "$running" = 1 ] && [ "$api_ready" != 1 ]; then errors="$errors api_unavailable"; fi
if [ -d "$HONK_LOCK_DIR" ]; then
if lock_owner_invalid || honk_lock_is_stale; then errors="$errors stale_operation_lock maintenance_recovery_required"; else errors="$errors operation_in_progress"; fi
fi
{ [ -e /etc/.honk-maintenance/journal ] || [ -L /etc/.honk-maintenance/journal ]; } && errors="$errors maintenance_recovery_required"
{ [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; } && errors="$errors update_recovery_required"
json_init
json_add_boolean enabled "$enabled"
json_add_boolean initialized "$initialized"
json_add_boolean boot_enabled "$boot_enabled"
json_add_boolean running "$running"
json_add_boolean api_ready "$api_ready"
json_add_string api_url "$api_url"
json_add_boolean clash_api_enabled "$clash_api_enabled"
json_add_int clash_api_port "$clash_api_port"
json_add_boolean clash_api_secret_set "$clash_secret_set"
json_add_boolean clash_api_ready "$clash_api_ready"
json_add_boolean clash_ui_ready "$clash_ui_ready"
json_add_string clash_api_url "$clash_api_url"
json_add_string core_version "$core_version"
json_add_string doona_version "$doona_version"
json_add_string kernel "$kernel"
json_add_string lan_network "$lan_network"
case "$listen_port" in
''|*[!0-9]*) listen_port=0 ;;
esac
json_add_int listen_port "$listen_port"
json_add_array errors
for error in $errors; do json_add_string "" "$error"; done
json_close_array
json_dump
}
service_ready_config() {
local enabled initialized
config_load honk
config_get_bool enabled main enabled 0
config_get_bool initialized main initialized 0
[ "$initialized" = 1 ]
}
start_job() {
job_operation=$1
job_request=
case "$job_operation" in backup|restore|reset|update_check|update_apply) ;; *) emit_result false invalid_operation; return 1 ;; esac
if [ "$job_operation" = update_apply ]; then
update_gate_result=$("$UPDATE" gate 2>/dev/null) || { emit_result false update_check_failed; return 1; }
json_load "$update_gate_result" 2>/dev/null || { emit_result false update_check_failed; return 1; }
json_get_var update_apply_enabled apply_enabled
if [ "$update_apply_enabled" != 1 ]; then
json_get_var update_gate_reason reason
case "$update_gate_reason" in rollback_tuple_unavailable|trusted_feed_unavailable|signed_manifest_missing|signed_manifest_expired|no_update) emit_result false "$update_gate_reason" ;; *) emit_result false update_check_failed ;; esac
return 1
fi
fi
if [ ! -e "$JOBS" ] && [ ! -L "$JOBS" ]; then
(umask 077; mkdir "$JOBS") || { emit_result false job_storage_unavailable; return 1; }
fi
[ -d "$JOBS" ] && [ ! -L "$JOBS" ] && [ "$(stat -c '%u:%a' "$JOBS" 2>/dev/null)" = 0:700 ] || { emit_result false job_storage_unavailable; return 1; }
job_id=$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-')
case "$job_id" in [0-9a-f][0-9a-f]*) [ "${#job_id}" -eq 32 ] || { emit_result false job_id_unavailable; return 1; } ;; *) emit_result false job_id_unavailable; return 1 ;; esac
job_dir=$JOBS/$job_id
mkdir "$job_dir" || { emit_result false job_storage_unavailable; return 1; }
chmod 0700 "$job_dir" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
json_init
json_add_string state running
json_add_string phase queued
json_add_boolean ok 1
json_add_string message job_started
json_dump > "$job_dir/status" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
chmod 0600 "$job_dir/status" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
nohup "$JOB_RUNNER" "$job_id" "$job_operation" >/dev/null 2>&1 </dev/null &
job_pid=$!
printf '%s\n' "$job_pid" > "$job_dir/worker.pid" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
chmod 0600 "$job_dir/worker.pid" || { rm -rf "$job_dir"; emit_result false job_storage_unavailable; return 1; }
json_init
json_add_boolean ok 1
json_add_string message job_started
json_add_string job_id "$job_id"
json_dump
}
job_status() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
json_get_var job_id job_id
case "$job_id" in ''|*[!0-9a-f]*) emit_result false invalid_job_id; return 1 ;; esac
[ "${#job_id}" -eq 32 ] || { emit_result false invalid_job_id; return 1; }
[ -d "$JOBS" ] && [ ! -L "$JOBS" ] && [ "$(stat -c '%u:%a' "$JOBS" 2>/dev/null)" = 0:700 ] || { emit_result false job_not_found; return 1; }
job_dir=$JOBS/$job_id
[ -d "$job_dir" ] && [ ! -L "$job_dir" ] && [ "$(stat -c '%u:%a' "$job_dir" 2>/dev/null)" = 0:700 ] || { emit_result false job_not_found; return 1; }
job_status_file=$JOBS/$job_id/status
[ -f "$job_status_file" ] && [ ! -L "$job_status_file" ] && [ "$(stat -c '%u:%a' "$job_status_file" 2>/dev/null)" = 0:600 ] || { emit_result false job_not_found; return 1; }
job_pid_file=$job_dir/worker.pid
[ -f "$job_pid_file" ] && [ ! -L "$job_pid_file" ] && [ "$(stat -c '%u:%a' "$job_pid_file" 2>/dev/null)" = 0:600 ] || { emit_result false job_not_found; return 1; }
job_state=$(jsonfilter -i "$job_status_file" -e '@.state' 2>/dev/null)
job_pid=$(cat "$job_pid_file" 2>/dev/null)
case "$job_pid" in ''|*[!0-9]*) job_pid= ;; esac
if [ "$job_state" = running ] && { [ -z "$job_pid" ] || ! kill -0 "$job_pid" 2>/dev/null; }; then
json_init
if [ -e /etc/.honk-maintenance/journal ] || [ -L /etc/.honk-maintenance/journal ] || [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ] || [ -d "$HONK_LOCK_DIR" ]; then
json_add_string state rollback_required
json_add_string phase recovery
job_lost_message=maintenance_recovery_required
else
json_add_string state failed
json_add_string phase complete
job_lost_message worker_lost
fi
json_add_boolean ok 0
json_add_string message "$job_lost_message"
json_dump > "$job_dir/status.new" && chmod 0600 "$job_dir/status.new" && mv -f "$job_dir/status.new" "$job_status_file" || { emit_result false job_status_unavailable; return 1; }
fi
cat "$job_status_file"
}
logs() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
json_get_var log_source source
json_get_var log_level level
json_get_var log_limit limit
case "$log_source" in service) log_pattern='honk.init|procd.*honk|honk service|honk\[[0-9]+\]' ;; core) log_pattern='honk-core|honk::' ;; maintenance) log_pattern='honk-maintenance|honk maintenance' ;; *) emit_result false invalid_log_source; return 1 ;; esac
case "$log_level" in all|debug|info|warn|error) ;; *) emit_result false invalid_log_level; return 1 ;; esac
case "$log_limit" in ''|*[!0-9]*) log_limit=200 ;; esac
[ "$log_limit" -ge 1 ] && [ "$log_limit" -le 200 ] || { emit_result false invalid_log_limit; return 1; }
command -v logread >/dev/null 2>&1 || { emit_result false logs_unavailable; return 1; }
[ -x "$REDACT" ] && [ -x "$LOG_LEVEL_FILTER" ] || { emit_result false logs_unavailable; return 1; }
log_raw_file=$(mktemp /tmp/honk-log.XXXXXX) || { emit_result false logs_unavailable; return 1; }
log_file=$(mktemp /tmp/honk-log.XXXXXX) || { rm -f "$log_raw_file"; emit_result false logs_unavailable; return 1; }
chmod 0600 "$log_raw_file" "$log_file" || { rm -f "$log_raw_file" "$log_file"; emit_result false logs_unavailable; return 1; }
log_capped=0
logread -l 200 -e 'honk' 2>/dev/null | head -c 262144 > "$log_raw_file"
log_raw_bytes=$(wc -c < "$log_raw_file" | tr -d ' ')
if [ "$log_raw_bytes" -ge 262144 ]; then
log_capped=1
log_raw_complete=$(mktemp /tmp/honk-log.XXXXXX) || { rm -f "$log_raw_file" "$log_file"; emit_result false logs_unavailable; return 1; }
chmod 0600 "$log_raw_complete" || { rm -f "$log_raw_file" "$log_file" "$log_raw_complete"; emit_result false logs_unavailable; return 1; }
sed '$d' "$log_raw_file" > "$log_raw_complete"
mv -f "$log_raw_complete" "$log_raw_file"
fi
grep -E "$log_pattern" "$log_raw_file" | "$LOG_LEVEL_FILTER" "$log_level" | "$REDACT" > "$log_file"
rm -f "$log_raw_file"
line_count=$(wc -l < "$log_file" | tr -d ' ')
if [ "$line_count" -gt "$log_limit" ]; then
log_capped=1
tail -n "$log_limit" "$log_file" > "$log_file.latest" && mv -f "$log_file.latest" "$log_file"
fi
log_bytes=$(wc -c < "$log_file" | tr -d ' ')
if [ "$log_bytes" -gt 32768 ]; then
log_capped=1
log_capped_file=$(mktemp /tmp/honk-log.XXXXXX) || { rm -f "$log_file"; emit_result false logs_unavailable; return 1; }
chmod 0600 "$log_capped_file" || { rm -f "$log_file" "$log_capped_file"; emit_result false logs_unavailable; return 1; }
LC_ALL=C awk 'BEGIN { first=1; bytes=0 } { lines[NR]=$0; sizes[NR]=length($0)+1; bytes+=sizes[NR]; while (bytes>32768 && first<=NR) { bytes-=sizes[first]; first++ } } END { for (i=first; i<=NR; i++) print lines[i] }' "$log_file" > "$log_capped_file"
mv -f "$log_capped_file" "$log_file"
fi
json_init
json_add_boolean ok 1
json_add_string message logs_read
[ "$log_capped" = 1 ] && json_add_boolean capped 1 || json_add_boolean capped 0
json_add_array lines
honk_add_log_lines "$log_file"
json_close_array
json_dump
rm -f "$log_file"
}
promote_default_route() {
local current=$1 candidate=$2 dns_block
if cmp -s "$current" /usr/share/honk/default-config.dae; then
sed 's/^ fallback: direct$/ fallback: proxy/' "$candidate" > "$candidate.route" || return 1
elif [ "$(sha256sum "$current" | awk '{print $1}')" = 83d1cc32bfe5b44ecae4b05c2c82c81ce9a36ab29f391d1e1d1d6b4993e17d2b ]; then
# The r1 package had only an include and a direct fallback. Preserve the
# newly added node or subscription while upgrading that exact default.
dns_block=$(sed -n '/^dns {$/,/^}$/p' /usr/share/honk/default-config.dae) || return 1
[ -n "$dns_block" ] || return 1
awk -v dns_block="$dns_block" '
/^routing[[:space:]]*\{$/ {
print "group {\n proxy {\n policy: min_moving_avg\n }\n}\n"
print dns_block "\n"
print
print " dip(224.0.0.0/3, \047ff00::/8\047) -> direct\n dip(geoip: private) && !dport(53) -> direct(must)\n dip(geoip: cn) && !dport(53) -> direct(must)\n domain(geosite: cn) -> direct"
next
}
/^ fallback: direct$/ { print " fallback: proxy"; next }
{ print }
' "$candidate" > "$candidate.route" || return 1
else
return 0
fi
mv "$candidate.route" "$candidate" || return 1
}
validate_proxy_nodes() (
local candidate=$1 provider_name=${2:-} node_prefix=${3:-} expected_nodes=${4:-0} temp port core_pid= attempt ready node_count group_count result provider
temp=$(mktemp -d /tmp/honk-onboard.XXXXXX) || return 1
validator_core_owned() {
[ -n "$core_pid" ] && [ -r "/proc/$core_pid/cmdline" ] || return 1
case "$(tr '\000' ' ' < "/proc/$core_pid/cmdline" 2>/dev/null)" in
*"$temp/config.dae"*--mock-ebpf*) [ "$(readlink -f "/proc/$core_pid/exe" 2>/dev/null)" = "$CORE" ] ;;
*) return 1 ;;
esac
}
validator_cleanup() {
if validator_core_owned; then
kill -TERM "$core_pid" 2>/dev/null || :
attempt=0
while validator_core_owned && [ "$attempt" -lt 3 ]; do sleep 1; attempt=$((attempt + 1)); done
if validator_core_owned; then
kill -KILL "$core_pid" 2>/dev/null || :
attempt=0
while validator_core_owned && [ "$attempt" -lt 2 ]; do sleep 1; attempt=$((attempt + 1)); done
fi
validator_core_owned && { logger -t honk "candidate core did not exit; retained $temp"; return 1; }
fi
[ -z "$core_pid" ] || wait "$core_pid" 2>/dev/null || :
rm -rf "$temp" || return 1
core_pid=
temp=
}
trap 'validator_cleanup' EXIT
trap 'exit 1' HUP INT TERM
port=$((20000 + $(hexdump -n 2 -e '1/2 "%u"' /dev/urandom) % 20000))
if ! cp -p /etc/honk/*.dae "$temp/" || ! cp "$candidate" "$temp/config.dae"; then
return 1
fi
cat > "$temp/system.dae" <<EOF
global {
wan_interface: auto
lan_interface: ''
data_dir: '$temp'
tproxy_port: $((port + 20000))
nfqueue_enable: false
store_subscribe: true
}
experimental {
native_api {
enabled: true
listen: '127.0.0.1:$port'
password_auth: false
allow_anonymous_loopback: true
config_write: false
}
}
EOF
DAE_LOCATION_ASSET=/usr/share/v2ray "$CORE" --config "$temp/config.dae" --mock-ebpf > "$temp/core.log" 2>&1 &
core_pid=$!
ready=0
result=2
attempt=0
while [ "$attempt" -lt 25 ]; do
if ! kill -0 "$core_pid" 2>/dev/null; then result=1; break; fi
if curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://127.0.0.1:$port/api/v1/nodes" -o "$temp/nodes.json" 2>/dev/null; then
ready=1
node_count=$(jsonfilter -i "$temp/nodes.json" -e '@.nodes[*].protocol' 2>/dev/null | awk '$1 != "direct" && $1 != "block" { count++ } END { print count+0 }')
if curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://127.0.0.1:$port/api/v1/groups" -o "$temp/groups.json" 2>/dev/null; then
group_count=$(jsonfilter -i "$temp/groups.json" -e '@[*].member_count' 2>/dev/null | awk '{ count += $1 } END { print count+0 }')
if [ "$node_count" -gt 0 ] && [ "$group_count" -gt 0 ]; then
if [ -n "$node_prefix" ]; then
node_count=$(jsonfilter -i "$temp/nodes.json" -e '@.nodes[*].name' 2>/dev/null | awk -v prefix="$node_prefix" '$0 ~ "^" prefix "[0-9]+$" { count++ } END { print count+0 }')
if [ "$node_count" -eq "$expected_nodes" ]; then result=0; break; fi
elif [ -z "$provider_name" ]; then result=0; break; fi
if curl -fsS --noproxy '*' --connect-timeout 1 --max-time 2 "http://127.0.0.1:$port/api/v1/providers" -o "$temp/providers.json" 2>/dev/null &&
jsonfilter -i "$temp/providers.json" -e '@.providers[*]' > "$temp/providers.rows" 2>/dev/null; then
while IFS= read -r provider; do
[ "$(jsonfilter -s "$provider" -e '@.name' 2>/dev/null)" = "$provider_name" ] || continue
[ "$(jsonfilter -s "$provider" -e '@.kind' 2>/dev/null)" = subscription ] || continue
node_count=$(jsonfilter -s "$provider" -e '@.node_count' 2>/dev/null)
case "$node_count" in ''|*[!0-9]*) break ;; esac
if [ "$node_count" -gt 0 ]; then result=0; fi
break
done < "$temp/providers.rows"
[ "$result" = 0 ] && break
fi
fi
fi
fi
sleep 1
attempt=$((attempt + 1))
done
[ "$ready" = 1 ] || result=1
validator_cleanup || return 1
trap - EXIT HUP INT TERM
return "$result"
)
commit_validated_candidate() {
local cfg=$1 candidate=$2 was_running=0 restore
[ -n "$(honk_lifecycle_managed_pid 2>/dev/null)" ] && was_running=1
cp -p "$cfg" "$cfg.bak" || return 1
chmod 0600 "$candidate" && mv "$candidate" "$cfg" || return 1
[ "$was_running" = 1 ] || return 0
honk_lifecycle_restart 30 && return 0
restore=$(mktemp /etc/honk/.config.rollback.XXXXXX) || return 3
cp -p "$cfg.bak" "$restore" && mv "$restore" "$cfg" || { rm -f "$restore"; return 3; }
honk_lifecycle_stop 30 && honk_lifecycle_start 30 && return 2
logger -t honk "candidate restart and previous service restoration failed; inspect $cfg.bak"
return 3
}
add_subscription() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
json_get_var sub_name name
json_get_var sub_url url
case "$sub_name" in
''|*[!A-Za-z0-9_-]*) emit_result false subscription_invalid; return 1 ;;
esac
case "$sub_url" in
http://*|https://*) ;;
*) emit_result false subscription_invalid; return 1 ;;
esac
cfg=/etc/honk/config.dae
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
begin_mutation
original_hash=$(sha256sum "$cfg" | awk '{print $1}')
if grep -qE "^[[:space:]]*${sub_name}:[[:space:]]*['\"]" "$cfg"; then
emit_result false subscription_invalid_or_conflicting
return 1
fi
candidate=$(mktemp /etc/honk/.config.dae.XXXXXX) || { emit_result false config_write_failed; return 1; }
if ! awk -v name="$sub_name" -v url="$sub_url" '
/^subscription[[:space:]]*\{/ {
print
printf " %s: \047%s\047 {\n interval: 86400s\n route: direct\n }\n", name, url
found = 1
next
}
{ print }
END {
if (!found) {
printf "\nsubscription {\n %s: \047%s\047 {\n interval: 86400s\n route: direct\n }\n}\n", name, url
}
}
' "$cfg" > "$candidate" 2>/dev/null; then
rm -f "$candidate"
emit_result false config_write_failed
return 1
fi
promote_default_route "$cfg" "$candidate" || { rm -f "$candidate" "$candidate.route"; emit_result false config_write_failed; return 1; }
validate_proxy_nodes "$candidate" "$sub_name"
case "$?" in
0) ;;
2) rm -f "$candidate"; emit_result false subscription_no_nodes; return 1 ;;
*) rm -f "$candidate"; emit_result false candidate_invalid; return 1 ;;
esac
[ "$(sha256sum "$cfg" | awk '{print $1}')" = "$original_hash" ] || { rm -f "$candidate"; emit_result false source_changed; return 1; }
commit_validated_candidate "$cfg" "$candidate"
case "$?" in
0) ;;
2) emit_result false reload_failed_rolled_back; return 1 ;;
3) emit_result false reload_and_rollback_failed; return 1 ;;
*) rm -f "$candidate"; emit_result false config_write_failed; return 1 ;;
esac
emit_result true subscription_added
}
add_nodes() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
json_get_var node_links links
[ -n "$node_links" ] || { emit_result false invalid_request; return 1; }
cfg=/etc/honk/config.dae
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
begin_mutation
original_hash=$(sha256sum "$cfg" | awk '{print $1}')
node_prefix="node_$(hexdump -n 4 -e '4/1 "%02x"' /dev/urandom)_"
node_lines=$(printf '%s\n' "$node_links" | awk -v prefix="$node_prefix" '
BEGIN { n = 0 }
{
if ($0 !~ /^(vless|vmess|ss|ssr|tuic|trojan|hysteria2|hy2|socks5|http|https):\/\//) next
n++
printf " %s%03d: \047%s\047\n", prefix, n, $0
}
END { if (n == 0) exit 1 }
') || { emit_result false share_links_invalid_or_conflicting; return 1; }
added_nodes=$(printf '%s\n' "$node_lines" | awk 'END { print NR }')
candidate=$(mktemp /etc/honk/.config.dae.XXXXXX) || { emit_result false config_write_failed; return 1; }
if ! awk -v lines="$node_lines" '
/^[[:space:]]*node[[:space:]]*\{/ {
print
printf "%s\n", lines
found = 1
next
}
{ print }
END {
if (!found) printf "\nnode {\n%s\n}\n", lines
}
' "$cfg" > "$candidate" 2>/dev/null; then
rm -f "$candidate"
emit_result false config_write_failed
return 1
fi
promote_default_route "$cfg" "$candidate" || { rm -f "$candidate" "$candidate.route"; emit_result false config_write_failed; return 1; }
validate_proxy_nodes "$candidate" "" "$node_prefix" "$added_nodes"
case "$?" in
0) ;;
2) rm -f "$candidate"; emit_result false share_links_invalid_or_conflicting; return 1 ;;
*) rm -f "$candidate"; emit_result false candidate_invalid; return 1 ;;
esac
[ "$(sha256sum "$cfg" | awk '{print $1}')" = "$original_hash" ] || { rm -f "$candidate"; emit_result false source_changed; return 1; }
commit_validated_candidate "$cfg" "$candidate"
case "$?" in
0) ;;
2) emit_result false reload_failed_rolled_back; return 1 ;;
3) emit_result false reload_and_rollback_failed; return 1 ;;
*) rm -f "$candidate"; emit_result false config_write_failed; return 1 ;;
esac
emit_result true nodes_added
}
extract_dae_block() {
# $1 = 块名, $2 = 文件;输出该顶级块(含大括号)。只匹配行首(无缩进)的块,
# 避免把 dns 里的 routing/request 子块误当成顶级 routing。
awk -v name="$1" '
$0 ~ "^" name "[[:space:]]*\\{" {
in_block = 1
depth = 0
}
in_block {
print
depth += gsub(/\{/, "{")
depth -= gsub(/\}/, "}")
if (depth <= 0 && $0 ~ /\}/) in_block = 0
}
' "$2"
}
import_dae() {
request=$(cat)
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
src=/tmp/honk-v2-upload/import.dae
[ -f "$src" ] || { emit_result false dae_upload_unavailable; return 1; }
cfg=/etc/honk/config.dae
[ -f "$cfg" ] || { emit_result false config_missing; return 1; }
# 提取 node 块(过滤 honk-core 不支持的 ssr/snell 协议)
node_block=$(awk '
$0 ~ /^node[[:space:]]*\{/ {
in_block = 1
depth = 0
print
next
}
in_block {
if ($0 ~ /node_[A-Za-z0-9_]+:[[:space:]]*\047(ssr|snell):\/\//) next
print
depth += gsub(/\{/, "{")
depth -= gsub(/\}/, "}")
if (depth <= 0 && $0 ~ /\}/) in_block = 0
}
' "$src")
group_block=$(extract_dae_block group "$src")
routing_block=$(extract_dae_block routing "$src")
dns_block=$(extract_dae_block dns "$src")
[ -n "$node_block" ] || [ -n "$group_block" ] || [ -n "$routing_block" ] || [ -n "$dns_block" ] || { emit_result false dae_invalid_or_unsupported; return 1; }
cp -p "$cfg" "$cfg.bak" 2>/dev/null || true
# 整体替换 node/group/routing/dns 四个业务块,保留 include(system.dae)与 subscription。
if ! awk -v node_block="$node_block" -v group_block="$group_block" -v routing_block="$routing_block" -v dns_block="$dns_block" '
function skip_block() {
depth = 1
while (depth > 0 && (getline line) > 0) {
depth += gsub(/\{/, "{", line)
depth -= gsub(/\}/, "}", line)
}
}
/^[[:space:]]*node[[:space:]]*\{/ {
if (node_block != "") { printf "%s\n", node_block; node_done = 1 }
skip_block()
next
}
/^[[:space:]]*group[[:space:]]*\{/ {
if (group_block != "") { printf "%s\n", group_block; group_done = 1 }
skip_block()
next
}
/^[[:space:]]*routing[[:space:]]*\{/ {
if (routing_block != "") { printf "%s\n", routing_block; routing_done = 1 }
skip_block()
next
}
/^[[:space:]]*dns[[:space:]]*\{/ {
if (dns_block != "") { printf "%s\n", dns_block; dns_done = 1 }
skip_block()
next
}
{ print }
END {
if (node_block != "" && !node_done) printf "\n%s\n", node_block
if (group_block != "" && !group_done) printf "\n%s\n", group_block
if (routing_block != "" && !routing_done) printf "\n%s\n", routing_block
if (dns_block != "" && !dns_done) printf "\n%s\n", dns_block
}
' "$cfg" > "$cfg.tmp" 2>/dev/null; then
rm -f "$cfg.tmp"
emit_result false config_write_failed
return 1
fi
mv "$cfg.tmp" "$cfg" || { rm -f "$cfg.tmp"; emit_result false config_write_failed; return 1; }
chmod 0600 "$cfg" 2>/dev/null || true
rm -f "$src" 2>/dev/null || true
if [ -n "$(honk_lifecycle_managed_pid 2>/dev/null)" ]; then
/etc/init.d/honk reload >/dev/null 2>&1 || :
fi
emit_result true dae_imported
}
set_enabled() {
uci set "honk.main.enabled=$1" && uci commit honk
}
procd_core_pid() {
honk_lifecycle_procd_pid
}
is_managed_core() {
[ "$1" = "$(honk_lifecycle_managed_pid 2>/dev/null)" ] && [ -n "$1" ]
}
begin_mutation() {
if [ -e /etc/.honk-update/journal ] || [ -L /etc/.honk-update/journal ]; then emit_result false update_recovery_required; exit 1; fi
honk_lock_acquire || { emit_result false operation_in_progress; exit 1; }
trap 'honk_lock_release >/dev/null 2>&1' 0
}
restore_prepare() {
# Stage the upload directory for a backup restore. The frontend calls
# this before uploading the archive to $RESTORE_PATH.
if ! mkdir -p "$RESTORE_ROOT" 2>/dev/null; then
emit_result false mkdir_failed
return 1
fi
rm -f "$RESTORE_PATH"
emit_result true prepared
}
case "$1" in
list)
echo '{"status":{},"check":{},"initialize":{"username":"String","password":"String"},"start":{},"stop":{},"restart":{},"settings":{"lan_network":"String","listen_port":"Integer","boot_enabled":"Boolean","clash_api_enabled":"Boolean","clash_api_port":"Integer","clash_api_secret":"String"},"add_subscription":{"name":"String","url":"String"},"add_nodes":{"links":"String"},"import_dae":{},"repair":{},"backup":{},"restore":{},"restore_prepare":{},"reset":{},"job_status":{"job_id":"String"},"logs":{"source":"String","level":"String","limit":"Integer"},"update_check":{},"update_apply":{}}'
;;
call)
case "$2" in
status)
status
;;
check)
if errors=$("$CONTROL" diagnose 2>/dev/null); then emit_errors true ""; else emit_errors false "$errors"; fi
;;
initialize)
exec "$CONTROL" initialize
;;
settings)
exec "$CONTROL" settings
;;
add_subscription)
add_subscription
;;
add_nodes)
add_nodes
;;
import_dae)
import_dae
;;
repair)
exec "$CONTROL" repair
;;
backup)
start_job backup
;;
restore)
start_job restore
;;
restore_prepare)
restore_prepare
;;
reset)
start_job reset
;;
job_status)
job_status
;;
logs)
logs
;;
update_check)
start_job update_check
;;
update_apply)
start_job update_apply
;;
start)
begin_mutation
if ! service_ready_config; then emit_result false not_initialized; exit 1; fi
old_enabled=$(uci -q get honk.main.enabled || echo 0)
if ! set_enabled 1; then emit_result false settings_write_failed; exit 1; fi
managed_pid=$(honk_lifecycle_managed_pid 2>/dev/null)
if [ -n "$managed_pid" ]; then
if honk_lifecycle_api_owned "$managed_pid"; then emit_result true already_running; else set_enabled "$old_enabled" >/dev/null 2>&1; emit_result false managed_service_unhealthy; exit 1; fi
elif honk_lifecycle_start 30; then
emit_result true started
else
honk_lifecycle_stop 30 >/dev/null 2>&1 || :
set_enabled "$old_enabled" >/dev/null 2>&1
emit_result false start_failed
exit 1
fi
;;
stop)
begin_mutation
old_enabled=$(uci -q get honk.main.enabled || echo 0)
if ! set_enabled 0; then emit_result false settings_write_failed; exit 1; fi
if honk_lifecycle_stop 30; then emit_result true stopped; else set_enabled "$old_enabled" >/dev/null 2>&1; emit_result false stop_failed; exit 1; fi
;;
restart)
begin_mutation
if ! service_ready_config; then emit_result false not_initialized; exit 1; fi
old_enabled=$(uci -q get honk.main.enabled || echo 0)
if ! set_enabled 1; then emit_result false settings_write_failed; exit 1; fi
if [ -n "$(honk_lifecycle_managed_pid 2>/dev/null)" ]; then
:
elif pidof honk-core >/dev/null 2>&1; then
set_enabled "$old_enabled" >/dev/null 2>&1
emit_result false unmanaged_core_running
exit 1
fi
if honk_lifecycle_restart 30; then emit_result true restarted; else set_enabled "$old_enabled" >/dev/null 2>&1; emit_result false restart_failed; exit 1; fi
;;
*)
emit_result false unknown_method
exit 1
;;
esac
;;
*)
exit 2
;;
esac
+199
View File
@@ -0,0 +1,199 @@
#!/bin/sh
# Download and install Dashboard for HONK
TARGET_DIR="${1:-}"
DOWNLOAD_URL="${2:-}"
LOG_FILE="/tmp/honk_dashboard_download.log"
STATUS_FILE="/tmp/honk_dashboard_download.status"
TMP_DIR="/tmp/dashboard_dl_$$"
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
set_status() {
echo "$1" > "$STATUS_FILE"
log "STATUS -> $1"
}
: > "$LOG_FILE"
if [ -z "$TARGET_DIR" ] || [ -z "$DOWNLOAD_URL" ]; then
log "Error: usage: $0 <target-dir> <archive-url>"
set_status "FAILED"
exit 1
fi
set_status "DOWNLOADING"
log "Deploying dashboard to: $TARGET_DIR"
log "Download URL: $DOWNLOAD_URL"
download_file() {
local url="$1" output="$2"
if command -v curl >/dev/null 2>&1; then
curl -s -S -k -L -f --connect-timeout 15 --max-time 180 -o "$output" "$url" >> "$LOG_FILE" 2>&1
elif command -v wget >/dev/null 2>&1; then
wget --no-check-certificate -T 15 -t 3 -O "$output" "$url" >> "$LOG_FILE" 2>&1
elif command -v uclient-fetch >/dev/null 2>&1; then
uclient-fetch --no-check-certificate --timeout 15 -O "$output" "$url" >> "$LOG_FILE" 2>&1
else
log "Error: No download tool found (curl, wget, or uclient-fetch)!"
return 1
fi
}
fetch_text() {
local url="$1"
if command -v curl >/dev/null 2>&1; then
curl -s -k -L -f --connect-timeout 8 --max-time 15 "$url" 2>/dev/null
elif command -v wget >/dev/null 2>&1; then
wget --no-check-certificate -q -T 8 -t 1 -O - "$url" 2>/dev/null
elif command -v uclient-fetch >/dev/null 2>&1; then
uclient-fetch --no-check-certificate -q --timeout 8 -O - "$url" 2>/dev/null
fi
}
# Resolve latest release for Doona repository URLs (non-archive links)
case "$DOWNLOAD_URL" in
*github.com/Zakkaus/doona|*github.com/Zakkaus/doona/)
PREFIX="${DOWNLOAD_URL%%https://github.com/*}"
log "Checking latest release..."
RESOLVED_URL=""
MAX_RETRIES=3
attempt=1
while [ "$attempt" -le "$MAX_RETRIES" ]; do
RELEASES_HTML=$(fetch_text "https://github.com/Zakkaus/doona/releases")
LATEST_TAG=$(printf "%s" "$RELEASES_HTML" | grep -o '/Zakkaus/doona/releases/tag/[^"/]*' | head -n 1 | sed 's|/Zakkaus/doona/releases/tag/||')
if [ -z "$LATEST_TAG" ]; then
RELEASES_HTML=$(fetch_text "https://github.com/Zakkaus/doona/releases.atom")
LATEST_TAG=$(printf "%s" "$RELEASES_HTML" | grep -o 'Zakkaus/doona/releases/tag/[^<"]*' | head -n 1 | sed 's|Zakkaus/doona/releases/tag/||')
fi
if [ -n "$LATEST_TAG" ]; then
log "Latest tag: $LATEST_TAG"
ASSETS_HTML=$(fetch_text "https://github.com/Zakkaus/doona/releases/expanded_assets/$LATEST_TAG")
ASSET_PATH=$(printf "%s" "$ASSETS_HTML" | grep -o '/Zakkaus/doona/releases/download/[^"]*/doona-[^"]*\.tar\.gz' | grep -v 'fonts' | head -n 1)
if [ -n "$ASSET_PATH" ]; then
RESOLVED_URL="https://github.com${ASSET_PATH}"
log "Found package: $ASSET_PATH"
else
TAG_NO_V="${LATEST_TAG#[vV]}"
RESOLVED_URL="https://github.com/Zakkaus/doona/releases/download/${LATEST_TAG}/doona-${TAG_NO_V}.tar.gz"
log "Inferred package URL: $RESOLVED_URL"
fi
break
fi
log "Failed to fetch latest tag (attempt $attempt/$MAX_RETRIES). Retrying in 2s..."
sleep 2
attempt=$((attempt + 1))
done
if [ -n "$RESOLVED_URL" ]; then
DOWNLOAD_URL="${PREFIX}${RESOLVED_URL}"
log "Resolved URL: $DOWNLOAD_URL"
else
log "Error: Failed to obtain latest release version after $MAX_RETRIES attempts."
set_status "FAILED"
exit 1
fi
;;
esac
normalize_target_dir() {
case "$TARGET_DIR" in
*..*) return 1 ;;
/etc/honk/*) ;;
*) return 1 ;;
esac
TARGET_DIR="${TARGET_DIR%/}"
local base="${TARGET_DIR##*/}"
case "$base" in
''|.|..) return 1 ;;
esac
mkdir -p /etc/honk 2>/dev/null || true
local canon_honk
canon_honk="$(cd /etc/honk 2>/dev/null && pwd -P)" || return 1
TARGET_DIR="$canon_honk/$base"
[ -L "$TARGET_DIR" ] && return 1
return 0
}
if ! normalize_target_dir; then
log "Error: Invalid target directory (must be a direct subdirectory of /etc/honk): $TARGET_DIR"
set_status "FAILED"
exit 1
fi
log "Downloading package..."
mkdir -p "$TMP_DIR"
ARCHIVE_FILE="$TMP_DIR/package.tmp"
if ! download_file "$DOWNLOAD_URL" "$ARCHIVE_FILE" || [ ! -s "$ARCHIVE_FILE" ]; then
log "Error: Download failed or file is empty."
set_status "FAILED"
exit 1
fi
log "Extracting archive..."
set_status "EXTRACTING"
EXTRACT_DIR="$TMP_DIR/extracted"
mkdir -p "$EXTRACT_DIR"
if case "$DOWNLOAD_URL" in *.tar.gz|*.tgz) true ;; *) false ;; esac; then
if ! command -v tar >/dev/null 2>&1; then
log "Error: 'tar' command not found!"
set_status "FAILED"
exit 1
fi
if ! tar -xzf "$ARCHIVE_FILE" -C "$EXTRACT_DIR" >> "$LOG_FILE" 2>&1; then
log "Error: Failed to extract tar.gz archive."
set_status "FAILED"
exit 1
fi
else
if ! command -v unzip >/dev/null 2>&1; then
log "Error: 'unzip' command not found! Please run 'opkg update && opkg install unzip'."
set_status "FAILED"
exit 1
fi
if ! unzip -o -q "$ARCHIVE_FILE" -d "$EXTRACT_DIR" >> "$LOG_FILE" 2>&1; then
log "Error: Failed to unzip archive."
set_status "FAILED"
exit 1
fi
fi
DEPLOY_SRC="$EXTRACT_DIR"
if [ ! -f "$DEPLOY_SRC/index.html" ]; then
DEPLOY_SRC="$(find "$EXTRACT_DIR" -maxdepth 3 -name "index.html" -exec dirname {} + 2>/dev/null | head -n 1)"
fi
if [ -z "$DEPLOY_SRC" ] || [ ! -f "$DEPLOY_SRC/index.html" ]; then
log "Error: index.html not found in extracted package."
set_status "FAILED"
exit 1
fi
log "Deploying files to $TARGET_DIR..."
mkdir -p "$TARGET_DIR"
rm -rf "$TARGET_DIR"/* "$TARGET_DIR"/.[!.]* 2>/dev/null || true
cp -rf "$DEPLOY_SRC/." "$TARGET_DIR/"
chmod -R 755 "$TARGET_DIR"
if [ -f "$TARGET_DIR/index.html" ]; then
log "Installed successfully."
set_status "SUCCESS"
exit 0
else
log "Error: Failed to deploy files to $TARGET_DIR."
set_status "FAILED"
exit 1
fi
@@ -1,38 +0,0 @@
#!/bin/sh
# geo-cron.sh enable|disable
# Manage the geo auto-update crontab entry. enable reads the schedule from
# honk.main.geo_auto_freq (daily|weekly); both actions are idempotent.
ACTION="$1"
case "$ACTION" in
enable|disable) ;;
*) echo "usage: $0 enable|disable" >&2; exit 64 ;;
esac
CRONTAB="/etc/crontabs/root"
TAG="# luci-app-honk geo-update"
SCRIPT="/usr/share/luci-app-honk/update-geo.sh"
# Drop any previous honk geo entry (tag line + its command line).
clean() {
[ -f "$CRONTAB" ] || return 0
grep -vF -e "$TAG" -e "$SCRIPT" "$CRONTAB" > "$CRONTAB.tmp" 2>/dev/null
mv "$CRONTAB.tmp" "$CRONTAB"
}
clean
if [ "$ACTION" = "enable" ]; then
case "$(uci -q get honk.main.geo_auto_freq)" in
weekly) sched="17 4 * * 0" ;; # Sunday 04:17
*) sched="17 4 * * *" ;; # every day 04:17
esac
mkdir -p /etc/crontabs
{
echo "$TAG"
echo "$sched $SCRIPT geoip; $SCRIPT geosite"
} >> "$CRONTAB"
/etc/init.d/cron enable >/dev/null 2>&1
fi
/etc/init.d/cron restart >/dev/null 2>&1
exit 0
@@ -1,34 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# pkg-info.sh <honk|luci-app-honk>
# Prints "<installed>\t<latest>" for the named package, either field empty when
# unknown. Used by the Maintenance view to decide whether to enable [升级].
PKG="$1"
case "$PKG" in
honk|luci-app-honk) ;;
*) printf '\t\n'; exit 64 ;;
esac
installed=""
latest=""
if command -v apk >/dev/null 2>&1; then
if apk info -e "$PKG" >/dev/null 2>&1; then
installed=$(apk list -I "$PKG" 2>/dev/null | awk -v p="$PKG" '
$1 ~ "^" p "-" {
sub("^" p "-", "", $1);
print $1;
exit
}
')
fi
latest=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '
$1 ~ "^" p "-[0-9]" {
v = $1; sub("^" p "-", "", v);
print v
}
' | sort -V | tail -1)
fi
printf '%s\t%s\n' "$installed" "$latest"
@@ -1,19 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# Background apk index refresh for the Maintenance view. Returns at once.
LOCK=/tmp/luci-app-honk.idx.lock
if [ -f "$LOCK" ]; then
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
[ "$(( $(date +%s) - mtime ))" -lt 90 ] && exit 0
fi
: > "$LOCK"
(
if command -v apk >/dev/null 2>&1; then
flock -n /tmp/luci-app-honk.apk.lock apk update
fi
) >/dev/null 2>&1 </dev/null &
exit 0
@@ -1,96 +0,0 @@
#!/bin/sh
# update-geo.sh <geoip|geosite>
# Fork-and-detach so the LuCI RPC call returns immediately. The maintenance
# card polls the .dat file's mtime/size to detect completion. A lock file
# in /tmp prevents concurrent runs.
TYPE="$1"
# Default source: Loyalsoldier/v2ray-rules-dat — daily-built, China-optimized,
# ships both geoip.dat and geosite.dat. Users can override per-type via
# honk.main.geoip_url / geosite_url (empty falls back to the default below).
case "$TYPE" in
geoip)
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
URL="$(uci -q get honk.main.geoip_url)"
DEST="/usr/share/v2ray/geoip.dat"
;;
geosite)
DEF_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
URL="$(uci -q get honk.main.geosite_url)"
DEST="/usr/share/v2ray/geosite.dat"
;;
*)
echo "usage: $0 <geoip|geosite>" >&2
exit 64
;;
esac
[ -n "$URL" ] || URL="$DEF_URL"
LOCK="/tmp/luci-app-honk.${TYPE}.lock"
LOG="/tmp/luci-app-honk.${TYPE}.log"
schedule_backend_reload() {
if /bin/pidof honk-core >/dev/null 2>&1; then
(
sleep 25
if /usr/bin/honk-core reload >/dev/null 2>&1; then
echo "$(date '+%F %T') reloaded honk-core"
else
echo "$(date '+%F %T') honk-core reload failed"
fi
) </dev/null >>"$LOG" 2>&1 &
fi
}
if [ -f "$LOCK" ]; then
# Lock is fresh (< 5 min)? Refuse. Stale? Remove and proceed.
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ]; then
echo "${TYPE} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
exit 75
fi
rm -f "$LOCK"
fi
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
echo "${TYPE} update already in progress" >&2
exit 75
fi
# Spawn detached worker — parent returns immediately to LuCI.
(
exec >"$LOG" 2>&1
trap 'rm -f "$LOCK"' EXIT INT TERM
rc=0
TMP="${DEST}.new"
mkdir -p "$(dirname "$DEST")"
echo "$(date '+%F %T') begin: $URL"
if ! curl -fsSL --proto '=http,https' --proto-redir '=http,https' --connect-timeout 15 --max-time 240 -o "$TMP" "$URL"; then
echo "$(date '+%F %T') download failed"
rm -f "$TMP"
rc=1
else
size=$(wc -c < "$TMP" 2>/dev/null || echo 0)
magic=$(hexdump -n 1 -e '1/1 "%02x"' "$TMP" 2>/dev/null)
if [ "$size" -lt 1024 ]; then
echo "$(date '+%F %T') file too small ($size bytes)"
rm -f "$TMP"
rc=2
elif [ "$magic" != "0a" ]; then
echo "$(date '+%F %T') invalid geodata format"
rm -f "$TMP"
rc=3
else
mv "$TMP" "$DEST"
echo "$(date '+%F %T') updated $DEST ($size bytes)"
schedule_backend_reload
fi
fi
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
exit 0
@@ -1,97 +0,0 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-only
# update-pkg.sh <honk|luci-app-honk>
# Refresh package indexes and upgrade the named package via apk. Forks to
# background; the result is streamed to /tmp/luci-app-honk.pkg.<name>.log.
PKG="$1"
case "$PKG" in
honk|luci-app-honk) ;;
*)
echo "usage: $0 <honk|luci-app-honk>" >&2
exit 64
;;
esac
# Run from a /tmp copy so upgrading luci-app-honk (which replaces this script)
# cannot corrupt the in-flight upgrade.
case "$0" in
/tmp/.honk-upd-*) ;;
*)
_self="/tmp/.honk-upd-$$"
cp "$0" "$_self" 2>/dev/null && exec sh "$_self" "$@"
;;
esac
LOCK="/tmp/luci-app-honk.pkg-${PKG}.lock"
LOG="/tmp/luci-app-honk.pkg-${PKG}.log"
if [ -f "$LOCK" ]; then
mtime=$(date -r "$LOCK" +%s 2>/dev/null || echo 0)
age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ]; then
echo "${PKG} update already in progress (PID $(cat "$LOCK" 2>/dev/null), age ${age}s)" >&2
exit 75
fi
rm -f "$LOCK"
fi
if ! ( set -C; echo "$$" >"$LOCK" ) 2>/dev/null; then
echo "${PKG} update already in progress" >&2
exit 75
fi
(
exec >"$LOG" 2>&1
trap 'rm -f "$LOCK"; [ "${0#/tmp/.honk-upd-}" != "$0" ] && rm -f "$0"' EXIT INT TERM
echo "$(date '+%F %T') begin upgrade: $PKG"
if command -v apk >/dev/null 2>&1; then
(
flock 9
apk update 2>&1
ver=$(apk list "$PKG" 2>/dev/null | awk -v p="$PKG" '$1 ~ "^" p "-[0-9]" { v=$1; sub("^" p "-", "", v); print v }' | sort -V | tail -1)
if [ -n "$ver" ]; then
constraint="$PKG=$ver"
else
echo "result: 软件源中没有找到 $PKG,请检查网络或软件源配置"
exit 1
fi
echo "--- apk add -s $constraint ---"
if ! apk add -s "$constraint" 2>&1; then
echo "note: apk 预检失败,通常是系统上其它软件包的问题,继续尝试升级"
fi
echo "--- apk add $constraint ---"
apk add "$constraint" 2>&1
exit 0
) 9>/tmp/luci-app-honk.apk.lock
rc=$?
if [ "$rc" != 0 ]; then
:
elif ! apk list --installed 2>/dev/null | grep -q "^${PKG}-"; then
echo "result: $PKG is not installed"
rc=1
elif apk list -u 2>/dev/null | grep -q "^${PKG}-"; then
echo "result: $PKG still has a pending upgrade"
rc=1
else
echo "result: $PKG is at the latest available version"
rc=0
fi
else
echo "no package manager found"
exit 3
fi
if [ "$rc" = 0 ]; then echo "$(date '+%F %T') ✓ 完成"; else echo "$(date '+%F %T') ✗ 失败 (rc=$rc)"; fi
if [ "$PKG" = "luci-app-honk" ] && [ "$rc" = "0" ]; then
echo "reloading rpcd to pick up new ACL"
/etc/init.d/rpcd reload 2>&1
fi
) </dev/null >/dev/null 2>&1 &
echo "started in background, see $LOG"
exit 0
@@ -1,58 +1,69 @@
{
"admin/services/honk": {
"title": "Honk",
"order": 35,
"title": "HONK",
"order": -1,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-honk" ]
"acl": [ "luci-app-honk" ],
"uci": { "honk": true }
}
},
"admin/services/honk/overview": {
"title": "Overview",
"order": 10,
"admin/services/honk/global": {
"title": "Global Settings",
"order": 1,
"action": {
"type": "view",
"path": "honk/overview"
"path": "honk/global"
}
},
"admin/services/honk/configuration": {
"title": "Configuration",
"order": 20,
"admin/services/honk/dns": {
"title": "DNS Settings",
"order": 2,
"action": {
"type": "view",
"path": "honk/configuration"
"path": "honk/dns"
}
},
"admin/services/honk/logs": {
"title": "Logs",
"order": 30,
"admin/services/honk/node": {
"title": "Node Settings",
"order": 3,
"action": {
"type": "view",
"path": "honk/logs"
"path": "honk/node"
}
},
"admin/services/honk/maintenance": {
"title": "Maintenance",
"order": 40,
"admin/services/honk/route": {
"title": "Routing Settings",
"order": 4,
"action": {
"type": "view",
"path": "honk/maintenance"
"path": "honk/route"
}
},
"admin/services/honk/api": {
"title": "API Settings",
"order": 5,
"action": {
"type": "view",
"path": "honk/api"
}
},
"admin/services/honk/dashboard": {
"hidden": true,
"title": "Dashboard",
"order": 6,
"action": {
"type": "alias",
"path": "admin/services/honk/overview"
"type": "view",
"path": "honk/dashboard"
}
},
"admin/services/honk/settings": {
"hidden": true,
"admin/services/honk/log": {
"title": "Logs",
"order": 7,
"action": {
"type": "alias",
"path": "admin/services/honk/overview"
"type": "view",
"path": "honk/log"
}
}
}
@@ -1,42 +1,29 @@
{
"luci-app-honk": {
"description": "Grant access to Honk controls and maintenance",
"description": "Grant access for luci-app-honk",
"read": {
"ubus": {
"honk": [ "status", "check", "job_status", "logs", "update_check" ],
"network": [ "get_proto_handlers" ],
"network.interface": [ "dump" ]
"luci.honk": [ "*" ]
},
"file": {
"/tmp/honk-backup.tar.gz": [ "read" ],
"/usr/share/v2ray/geoip.dat": [ "read" ],
"/usr/share/v2ray/geosite.dat": [ "read" ],
"/tmp/luci-app-honk.geoip.log": [ "read" ],
"/tmp/luci-app-honk.geosite.log": [ "read" ],
"/tmp/luci-app-honk.pkg-honk.log": [ "read" ],
"/tmp/luci-app-honk.pkg-luci-app-honk.log": [ "read" ],
"/usr/share/luci-app-honk/pkg-info.sh honk": [ "exec" ],
"/usr/share/luci-app-honk/pkg-info.sh luci-app-honk": [ "exec" ],
"/usr/share/luci-app-honk/refresh-index.sh": [ "exec" ]
},
"uci": [ "network", "honk" ]
"uci": [ "honk" ],
"file": [
"/etc/honk/config.dae",
"/etc/honk/config.d/*",
"/etc/honk/doona/*",
"/etc/honk/zashboard/*"
]
},
"write": {
"cgi-io": [ "upload" ],
"ubus": {
"honk": [ "initialize", "start", "stop", "restart", "settings", "add_subscription", "add_nodes", "import_dae", "repair", "backup", "restore", "restore_prepare", "reset", "update_apply" ]
"luci.honk": [ "*" ]
},
"file": {
"/tmp/honk-maintenance/restore.tar.gz": [ "write" ],
"/tmp/honk-v2-upload/import.dae": [ "write" ],
"/usr/share/luci-app-honk/update-geo.sh geoip": [ "exec" ],
"/usr/share/luci-app-honk/update-geo.sh geosite": [ "exec" ],
"/usr/share/luci-app-honk/geo-cron.sh enable": [ "exec" ],
"/usr/share/luci-app-honk/geo-cron.sh disable": [ "exec" ],
"/usr/share/luci-app-honk/update-pkg.sh honk": [ "exec" ],
"/usr/share/luci-app-honk/update-pkg.sh luci-app-honk": [ "exec" ]
},
"uci": [ "honk" ]
"uci": [ "honk" ],
"file": [
"/etc/honk/config.dae",
"/etc/honk/config.d/*",
"/etc/honk/doona/*",
"/etc/honk/zashboard/*"
]
}
}
}
@@ -0,0 +1,602 @@
#!/usr/bin/ucode
'use strict';
import { readfile, writefile, popen, stat } from 'fs';
import { cursor } from 'uci';
const DEFAULT_HOST = "0.0.0.0";
const DEFAULT_PORT = "9527";
const DEFAULT_SECRET = "honk114514";
const DASHBOARD_DIRS = {
zashboard: "/etc/honk/zashboard",
doona: "/etc/honk/doona"
};
function get_dashboard_dir(type) {
return DASHBOARD_DIRS[type] || DASHBOARD_DIRS.doona;
}
function is_valid_dashboard_type(type) {
if (type == "none")
return true;
for (let known in keys(DASHBOARD_DIRS))
if (known == type)
return true;
return false;
}
function is_safe_ui_dir(path) {
if (!path || !match(path, /^\/etc\/honk\/[^\/]+$/))
return false;
let base = substr(path, 10);
return base != "." && base != "..";
}
let cached_pid = null;
function is_honk_pid(pid) {
if (!pid)
return false;
let comm = readfile("/proc/" + pid + "/comm");
if (comm)
return trim(comm) == "honk-core";
let cmd = readfile("/proc/" + pid + "/cmdline");
return cmd ? match(cmd, /honk-core/) : false;
}
function get_honk_pid() {
if (cached_pid && is_honk_pid(cached_pid))
return cached_pid;
cached_pid = null;
let p = popen("pidof honk-core 2>/dev/null");
let pids = p ? p.read("all") : "";
if (p) p.close();
let m = match(pids, /([0-9]+)/);
cached_pid = m ? m[1] : null;
return cached_pid;
}
function parse_host_port(addr) {
let res = { host: "", port: "" };
if (!addr) return res;
let m_v6 = match(addr, /^\[([^\]]+)\]:([0-9]+)$/);
let m_v4 = match(addr, /^([^:]+):([0-9]+)$/);
let m_p = match(addr, /^:([0-9]+)$/);
if (m_v6) {
res.host = m_v6[1];
res.port = m_v6[2];
} else if (m_v4) {
res.host = m_v4[1];
res.port = m_v4[2];
} else if (m_p) {
res.host = "0.0.0.0";
res.port = m_p[1];
} else if (match(addr, /^[0-9]+$/)) {
res.host = "0.0.0.0";
res.port = addr;
}
return res;
}
function strip_dae_comments(content) {
if (!content) return "";
let lines = split(content, /[\r\n]+/);
let clean_lines = [];
for (let idx, line in lines) {
let in_single = false;
let in_double = false;
let clean = "";
let len = length(line);
for (let i = 0; i < len; i++) {
let c = substr(line, i, 1);
let next_c = (i + 1 < len) ? substr(line, i + 1, 1) : "";
if (c == "'" && !in_double) {
in_single = !in_single;
clean += c;
} else if (c == '"' && !in_single) {
in_double = !in_double;
clean += c;
} else if (!in_single && !in_double) {
if (c == '#' || (c == '/' && next_c == '/')) {
break;
} else {
clean += c;
}
} else {
clean += c;
}
}
if (match(clean, /\S/)) {
push(clean_lines, clean);
}
}
return join("\n", clean_lines);
}
function find_bracket_block(content, header_regex) {
let lines = split(content, "\n");
let start_idx = -1;
let end_idx = -1;
let in_block = false;
let depth = 0;
let in_single = false;
let in_double = false;
for (let idx, line in lines) {
let trimmed = trim(line);
if (!in_block && match(trimmed, header_regex)) {
in_block = true;
start_idx = idx;
}
if (in_block) {
let len = length(line);
for (let i = 0; i < len; i++) {
let c = substr(line, i, 1);
let next_c = (i + 1 < len) ? substr(line, i + 1, 1) : "";
if (c == "'" && !in_double) {
in_single = !in_single;
} else if (c == '"' && !in_single) {
in_double = !in_double;
} else if (!in_single && !in_double) {
if (c == '#' || (c == '/' && next_c == '/')) {
break;
} else if (c == '{') {
depth++;
} else if (c == '}') {
depth--;
if (depth <= 0) {
end_idx = idx;
break;
}
}
}
}
if (end_idx != -1) break;
}
}
return { start: start_idx, end: end_idx };
}
function extract_bracket_block(content, header_regex) {
let bounds = find_bracket_block(content, header_regex);
if (bounds.start == -1 || bounds.end == -1) return null;
let lines = split(content, "\n");
let block_lines = [];
for (let i = bounds.start; i <= bounds.end; i++) {
push(block_lines, lines[i]);
}
return join("\n", block_lines);
}
function remove_bracket_block(content, header_regex) {
let bounds = find_bracket_block(content, header_regex);
if (bounds.start == -1 || bounds.end == -1) return content;
let lines = split(content, "\n");
let new_lines = [];
for (let i = 0; i < length(lines); i++) {
if (i < bounds.start || i > bounds.end) {
push(new_lines, lines[i]);
}
}
return join("\n", new_lines);
}
function parse_native_api(clean_content) {
let block = extract_bracket_block(clean_content, /native_api\s*\{/);
if (!block) return null;
let listen_m = match(block, /listen\s*:\s*['"]?([^'" \t\r\n]+)['"]?/);
let ui_m = match(block, /ui\s*:\s*['"]?([^'" \t\r\n]+)['"]?/);
let sec_m = match(block, /secret\s*:\s*['"]?([^'" \t\r\n]*)['"]?/);
let en_m = match(block, /enabled\s*:\s*['"]?(true|false)['"]?/);
let cw_m = match(block, /config_write\s*:\s*['"]?(true|false)['"]?/);
let res = {
enabled: en_m ? (en_m[1] == "true") : true,
config_write: cw_m ? (cw_m[1] == "true") : true,
listen: listen_m ? listen_m[1] : "",
ui: ui_m ? ui_m[1] : "",
secret: sec_m ? sec_m[1] : "",
host: "",
port: ""
};
let hp = parse_host_port(res.listen);
res.host = hp.host;
res.port = hp.port;
return res;
}
function get_config_file_path() {
let u = cursor();
let p = null;
if (u) {
u.load("honk");
p = u.get("honk", "config", "config_file");
}
return p || "/etc/honk/config.dae";
}
function get_api_file_path() {
return "/etc/honk/config.d/api.dae";
}
function get_api_config() {
let api_file = get_api_file_path();
let content = readfile(api_file) || "";
let clean = strip_dae_comments(content);
let parsed_native = parse_native_api(clean);
if (!parsed_native) {
let config_file = get_config_file_path();
let legacy_content = readfile(config_file) || "";
let legacy_clean = strip_dae_comments(legacy_content);
let leg_native = parse_native_api(legacy_clean);
if (leg_native) {
return {
file: config_file,
parsed_native: leg_native,
is_legacy: true
};
}
}
return {
file: api_file,
parsed_native: parsed_native,
is_legacy: false
};
}
function get_uci_dashboard_type(u) {
if (!u) return null;
u.load("honk");
let requested_type = u.get("honk", "config", "dashboard");
if (!requested_type) {
u.foreach("honk", "honk", function(s) {
if (s.dashboard) {
requested_type = s.dashboard;
return false;
}
});
}
return requested_type;
}
function clean_legacy_api_from_config(config_file) {
if (!match(config_file, /^\/etc\/honk\//) || match(config_file, /\.\./))
return;
let main_content = readfile(config_file);
if (!main_content) return;
let cleaned_main = remove_bracket_block(main_content, /^[#\/]*\s*clash_api\s*\{/);
cleaned_main = remove_bracket_block(cleaned_main, /^[#\/]*\s*native_api\s*\{/);
let exp_block = extract_bracket_block(cleaned_main, /experimental\s*\{/);
if (exp_block) {
let inner_clean = strip_dae_comments(exp_block);
inner_clean = replace(inner_clean, /experimental\s*\{/, "");
inner_clean = replace(inner_clean, /\}[ \t\r\n]*$/, "");
if (!match(inner_clean, /\S/)) {
cleaned_main = remove_bracket_block(cleaned_main, /^[#\/]*\s*experimental\s*\{/);
}
}
cleaned_main = replace(cleaned_main, /[ \t]*experimental\s*\{\s*\}[ \t]*\n?/, "");
if (cleaned_main != main_content) {
writefile(config_file, cleaned_main);
}
}
function get_dashboard_info(req) {
let u = cursor();
let requested_type = (req && req.args) ? req.args.type : null;
if (!is_valid_dashboard_type(requested_type) && u) {
requested_type = get_uci_dashboard_type(u);
}
if (!is_valid_dashboard_type(requested_type)) requested_type = "none";
let api_cfg = get_api_config();
let parsed_native = api_cfg.parsed_native;
let running = !!get_honk_pid();
let res = {
dashboard_type: requested_type,
configured: false,
has_ui: false,
running: running,
config_file: api_cfg.file,
external_controller: "",
host: "",
port: "",
secret: "",
external_ui: ""
};
if (requested_type == "none") {
return res;
}
let target_default_ui = get_dashboard_dir(requested_type);
let other_type = (requested_type == "zashboard") ? "doona" : "zashboard";
let other_default_ui = get_dashboard_dir(other_type);
if (parsed_native && parsed_native.enabled && parsed_native.listen) {
res.external_controller = parsed_native.listen;
res.host = parsed_native.host;
res.port = parsed_native.port || DEFAULT_PORT;
res.secret = parsed_native.secret;
res.external_ui = (parsed_native.ui && parsed_native.ui != other_default_ui) ? parsed_native.ui : target_default_ui;
let index_path = res.external_ui + "/index.html";
let s = stat(index_path);
res.has_ui = (s && s.type == "file") ? true : false;
if (!res.has_ui) {
res.configured = true;
} else if (parsed_native.ui && parsed_native.ui != other_default_ui) {
res.configured = true;
} else {
res.configured = false;
}
}
return res;
}
function download_dashboard(req) {
let requested_type = (req && req.args) ? req.args.type : null;
if (!is_valid_dashboard_type(requested_type)) {
requested_type = get_uci_dashboard_type(cursor());
}
if (!is_valid_dashboard_type(requested_type) || requested_type == "none") {
requested_type = "doona";
}
let url = (req && req.args && req.args.url) ? req.args.url : "";
if (!match(url, /^https?:\/\//) || match(url, /[ \t\r\n'"`]/)) {
return { success: false, message: "Invalid URL" };
}
let api_cfg = get_api_config();
let parsed_native = api_cfg.parsed_native;
let other_type = (requested_type == "zashboard") ? "doona" : "zashboard";
let other_default_ui = get_dashboard_dir(other_type);
let target_dir = get_dashboard_dir(requested_type);
if (parsed_native && parsed_native.ui && parsed_native.ui != other_default_ui &&
is_safe_ui_dir(parsed_native.ui)) {
target_dir = parsed_native.ui;
}
if (!is_safe_ui_dir(target_dir))
return { success: false, message: "Refusing to deploy to an unsafe target directory" };
let script = "/usr/share/honk/download_dashboard.sh";
let s = stat(script);
if (!s) {
return { success: false, message: "Script not found: " + script };
}
let safe_script = replace(script, "'", "'\\''");
let safe_target = replace(target_dir, "'", "'\\''");
let safe_url = replace(url, "'", "'\\''");
let cmd = sprintf("/bin/sh '%s' '%s' '%s' >/dev/null 2>&1 &", safe_script, safe_target, safe_url);
let rc = system(cmd);
if (rc != 0)
return { success: false, message: "failed to dispatch the download task" };
return { success: true, target_dir: target_dir, url: url, queued: true };
}
function switch_dashboard_api(target_type) {
cached_pid = null;
if (!is_valid_dashboard_type(target_type))
return { success: false, message: "Invalid dashboard type" };
let api_file = get_api_file_path();
let config_file = get_config_file_path();
let api_cfg = get_api_config();
let parsed_native = api_cfg.parsed_native;
clean_legacy_api_from_config(config_file);
system("mkdir -p /etc/honk/config.d");
let u = cursor();
if (u) {
u.load("honk");
let sid = "config";
if (!u.get("honk", sid)) {
u.foreach("honk", "honk", function(s) { sid = s[".name"]; });
}
u.set("honk", sid, "dashboard", target_type || "none");
u.commit("honk");
}
if (target_type == "none" || !target_type) {
let cur = readfile(api_file);
if (cur && trim(cur) != "") {
writefile(api_file, "");
system("/etc/init.d/honk restart >/dev/null 2>&1 &");
return { success: true, type: "none", queued: true };
}
writefile(api_file, "");
return { success: true, type: "none" };
}
let target_ui = get_dashboard_dir(target_type);
let index_path = target_ui + "/index.html";
let s = stat(index_path);
let has_target_ui = (s && s.type == "file") ? true : false;
// If target UI files do not exist yet, do not point api.dae to a non-existent UI directory
// and do not restart honk into a fatal error if native API is already working.
if (!has_target_ui && !api_cfg.is_legacy && parsed_native && parsed_native.enabled && parsed_native.listen) {
return { success: true, type: target_type, has_ui: false, pending_download: true };
}
// Check if already correctly configured in api.dae
if (!api_cfg.is_legacy && parsed_native && parsed_native.enabled &&
parsed_native.listen &&
parsed_native.config_write &&
parsed_native.ui == target_ui &&
has_target_ui) {
return { success: true, type: target_type };
}
let api_content = readfile(api_file) || "";
let sec = (parsed_native && parsed_native.secret && length(parsed_native.secret) >= 8) ? parsed_native.secret : DEFAULT_SECRET;
let listen = (parsed_native && parsed_native.listen) ? parsed_native.listen : (DEFAULT_HOST + ":" + DEFAULT_PORT);
if (!parsed_native) {
let clean = strip_dae_comments(api_content);
let sec_m = match(clean, /secret\s*:\s*['"]?([^'" \t\r\n]*)['"]?/);
let listen_m = match(clean, /listen\s*:\s*['"]?([^'" \t\r\n]+)['"]?/);
if (sec_m && length(sec_m[1]) >= 8) {
sec = sec_m[1];
}
if (listen_m) {
listen = listen_m[1];
}
}
let cleaned_api = api_content;
for (let iter = 0; iter < 10; iter++) {
let before = cleaned_api;
cleaned_api = remove_bracket_block(cleaned_api, /^[#\/]*\s*clash_api\s*\{/);
cleaned_api = remove_bracket_block(cleaned_api, /^[#\/]*\s*native_api\s*\{/);
cleaned_api = remove_bracket_block(cleaned_api, /^[#\/]*\s*experimental\s*\{/);
if (cleaned_api == before)
break;
}
cleaned_api = replace(cleaned_api, /[ \t]*experimental\s*\{\s*\}[ \t]*\n?/, "");
let ui_line = has_target_ui ? (" ui: '" + target_ui + "'\n") : "";
let api_inner =
" native_api {\n" +
" enabled: true\n" +
" listen: '" + listen + "'\n" +
" secret: '" + sec + "'\n" +
ui_line +
" config_write: true\n" +
" }\n";
let default_block = "experimental {\n" + api_inner + "}\n";
let base = trim(cleaned_api);
let new_api_content;
if (base == "") {
new_api_content = "# api.dae\n# Configure API access for HONK dashboards and controllers.\n\n" + default_block;
} else {
new_api_content = base + "\n\n" + default_block;
}
writefile(api_file, new_api_content);
system("/etc/init.d/honk restart >/dev/null 2>&1 &");
return { success: true, type: target_type, queued: true };
}
return {
"luci.honk": {
status: {
call: function(req) {
let pid = get_honk_pid();
let running = !!pid;
let memory = null;
if (running) {
let status_str = readfile("/proc/" + pid + "/status");
if (status_str) {
let rss_m = match(status_str, /VmRSS:\s+([0-9]+)\s+kB/);
if (rss_m) {
memory = sprintf("%.1f MB", +rss_m[1] / 1024);
}
}
}
return { running: running, memory: memory };
}
},
reload: {
call: function(req) {
cached_pid = null;
let rc = system("/etc/init.d/honk hot_reload >/dev/null 2>&1 &");
if (rc != 0)
return { success: false, message: "failed to dispatch hot_reload" };
return { success: true, queued: true };
}
},
restart: {
call: function(req) {
cached_pid = null;
let rc = system("/etc/init.d/honk restart >/dev/null 2>&1 &");
if (rc != 0)
return { success: false, message: "failed to dispatch restart" };
return { success: true, queued: true };
}
},
get_log: {
call: function(req) {
let p = popen("tail -n 1000 /var/log/honk/honk.log 2>/dev/null");
let content = p ? p.read("all") : "";
if (p) p.close();
return { log: content || "" };
}
},
clear_log: {
call: function(req) {
system("mkdir -p /var/log/honk");
writefile("/var/log/honk/honk.log", "");
return { success: true };
}
},
get_dashboard_info: {
args: { type: "string" },
call: get_dashboard_info
},
download_dashboard: {
args: { url: "string", type: "string" },
call: download_dashboard
},
download_status: {
call: function(req) {
let status_raw = readfile("/tmp/honk_dashboard_download.status") || "IDLE";
let log = readfile("/tmp/honk_dashboard_download.log") || "";
let status_m = match(status_raw, /\S+/);
let status = status_m ? status_m[0] : "IDLE";
return { status: status, log: log };
}
},
switch_dashboard_api: {
args: { type: "string" },
call: function(req) {
let t = req.args ? req.args.type : "none";
return switch_dashboard_api(t);
}
}
}
};