mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-14 04:07:56 +08:00
1258 lines
51 KiB
Bash
Executable File
1258 lines
51 KiB
Bash
Executable File
#!/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
|
||
DAE_UPLOAD_ROOT=/tmp/honk-v2-upload
|
||
DAE_UPLOAD_PATH=$DAE_UPLOAD_ROOT/import.dae
|
||
DAE_UPLOAD_LIMIT=2097152
|
||
|
||
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 core_commit 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 settings_owner api_enabled core_ready config_pending draft_pending legacy_draft clash_listen_addr
|
||
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
|
||
# 只有 system.dae 负责这些字段时,UCI 里的端口/密钥才有意义。
|
||
settings_owner=$(config_owner /etc/honk/config.dae)
|
||
api_enabled=0
|
||
clash_api_enabled=0
|
||
clash_api_port=0
|
||
clash_secret_set=0
|
||
clash_listen_addr=
|
||
core_ready=0
|
||
config_pending=0
|
||
draft_pending=0
|
||
legacy_draft=0
|
||
listen=
|
||
runtime_config=$("$CORE" config validate --config /etc/honk/config.dae 2>/dev/null)
|
||
if [ -n "$runtime_config" ]; then
|
||
json_load "$runtime_config" 2>/dev/null
|
||
json_select runtime 2>/dev/null
|
||
json_get_var api_enabled native_api_enabled
|
||
json_get_var listen native_api_listen
|
||
json_get_var clash_listen_addr clash_api_listen
|
||
json_get_var clash_secret_set clash_api_secret_set
|
||
json_select .. 2>/dev/null
|
||
fi
|
||
case "$api_enabled" in true) api_enabled=1 ;; *) api_enabled=0 ;; esac
|
||
case "$clash_secret_set" in true) clash_secret_set=1 ;; *) clash_secret_set=0 ;; esac
|
||
if [ -n "$managed_pid" ] && honk_lifecycle_ready_owned "$managed_pid"; then core_ready=1; fi
|
||
if [ "$core_ready" = 1 ]; then
|
||
runtime_token=$(awk 'NR == 1 { print $1 }' /var/run/honk/readiness 2>/dev/null)
|
||
runtime_marker=/var/run/honk/core-ready-$runtime_token
|
||
runtime_api_enabled=$(sed -n 's/^api_enabled=//p' "$runtime_marker" 2>/dev/null)
|
||
runtime_listen=$(sed -n 's/^api_listen=//p' "$runtime_marker" 2>/dev/null)
|
||
runtime_clash_listen=$(sed -n 's/^clash_api_listen=//p' "$runtime_marker" 2>/dev/null)
|
||
runtime_clash_secret_set=$(sed -n 's/^clash_api_secret_set=//p' "$runtime_marker" 2>/dev/null)
|
||
case "$runtime_api_enabled" in true) api_enabled=1 ;; false) api_enabled=0 ;; esac
|
||
[ -n "$runtime_listen" ] && listen=$runtime_listen
|
||
clash_listen_addr=$runtime_clash_listen
|
||
case "$runtime_clash_secret_set" in true) clash_secret_set=1 ;; false) clash_secret_set=0 ;; esac
|
||
fi
|
||
if [ -n "$clash_listen_addr" ]; then
|
||
clash_api_enabled=1
|
||
clash_api_port=${clash_listen_addr##*:}
|
||
fi
|
||
case "$listen" in
|
||
''|*[!0-9.:]*) listen="127.0.0.1:$listen_port" ;;
|
||
esac
|
||
api_url="http://$listen"
|
||
[ "$api_enabled" = 1 ] || api_url=""
|
||
clash_api_url=
|
||
[ -n "$clash_listen_addr" ] && clash_api_url="http://$clash_listen_addr"
|
||
active_hash=$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')
|
||
runtime_hash=$(awk 'NR == 1 { print $2 }' /var/run/honk/readiness 2>/dev/null)
|
||
[ -n "$runtime_hash" ] && [ "$runtime_hash" = "$active_hash" ] || { [ "$running" = 1 ] && config_pending=1; }
|
||
# 草稿与活动配置分开标记:草稿存在不代表已生效,也不参与运行态判断。
|
||
if [ -f /etc/honk/config-draft.dae ] && [ ! -L /etc/honk/config-draft.dae ]; then
|
||
draft_hash=$(sha256sum /etc/honk/config-draft.dae 2>/dev/null | awk '{print $1}')
|
||
[ "$draft_hash" = "$active_hash" ] || draft_pending=1
|
||
fi
|
||
[ -f /etc/honk/manual-config.dae ] && [ ! -L /etc/honk/manual-config.dae ] && legacy_draft=1
|
||
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
|
||
case "$clash_api_port" in
|
||
''|*[!0-9]*) clash_api_port=0 ;;
|
||
esac
|
||
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
|
||
# --version 只是构建时注入的版本串,真正的上游代码看 provenance.json
|
||
core_commit=$(jsonfilter -i /usr/share/honk/provenance.json -e '@.core.commit' 2>/dev/null)
|
||
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 ] && [ "$api_enabled" = 1 ]; then errors="$errors api_unavailable"; fi
|
||
if [ "$running" = 1 ] && [ "$core_ready" != 1 ]; then errors="$errors core_not_ready"; 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_string settings_owner "$settings_owner"
|
||
json_add_boolean config_pending "$config_pending"
|
||
json_add_boolean draft_pending "$draft_pending"
|
||
json_add_boolean legacy_draft "$legacy_draft"
|
||
json_add_boolean core_ready "$core_ready"
|
||
json_add_boolean api_enabled "$api_enabled"
|
||
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 core_commit "$core_commit"
|
||
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"
|
||
}
|
||
|
||
# 判出厂配置时忽略本机直连区块:启动脚本写入本机地址后逐字比较必然失败。
|
||
strip_local_direct() {
|
||
awk -v begin=' # honk:local-direct-begin' -v end=' # honk:local-direct-end' '
|
||
$0 == begin {
|
||
if (skip) bad = 1
|
||
b++
|
||
skip = 1
|
||
print
|
||
next
|
||
}
|
||
$0 == end {
|
||
if (!skip) bad = 1
|
||
e++
|
||
skip = 0
|
||
print
|
||
next
|
||
}
|
||
skip { next }
|
||
{ print }
|
||
END { if (bad || skip || b != 1 || e != 1) exit 1 }
|
||
' "$1"
|
||
}
|
||
|
||
# 标记缺失、重复或不成对时一律不认定为出厂配置,避免误改用户出口。
|
||
is_factory_config() {
|
||
local current=$1 factory=/usr/share/honk/default-config.dae a b rc
|
||
[ -f "$factory" ] && [ ! -L "$factory" ] || return 1
|
||
a=$(mktemp /tmp/honk-route.XXXXXX) || return 1
|
||
b=$(mktemp /tmp/honk-route.XXXXXX) || { rm -f "$a"; return 1; }
|
||
if strip_local_direct "$current" > "$a" && strip_local_direct "$factory" > "$b"; then
|
||
cmp -s "$a" "$b"
|
||
rc=$?
|
||
else
|
||
rc=1
|
||
fi
|
||
rm -f "$a" "$b"
|
||
return "$rc"
|
||
}
|
||
|
||
promote_default_route() {
|
||
local current=$1 candidate=$2 dns_block
|
||
if is_factory_config "$current"; 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))
|
||
# 先带完整文件树校验原始候选(候选名不是 .dae 结尾,复制成 candidate-original.dae)。
|
||
if ! cp -p /etc/honk/*.dae "$temp/" || ! cp -p "$candidate" "$temp/candidate-original.dae"; then
|
||
rm -rf "$temp"
|
||
return 1
|
||
fi
|
||
if ! "$CORE" config validate --config "$temp/candidate-original.dae" >/dev/null 2>&1; then
|
||
rm -rf "$temp"
|
||
return 2
|
||
fi
|
||
# 候选原样作为被测配置,落盘路径与 API 由我们写的 system.dae 提供。
|
||
cp -p "$candidate" "$temp/config.dae" || { rm -rf "$temp"; return 1; }
|
||
chmod 0600 "$temp/config.dae" || { rm -rf "$temp"; return 1; }
|
||
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 \
|
||
--data-dir "$temp" --log-file "$temp/core.log" > "$temp/core.console" 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
|
||
}
|
||
|
||
# 添加订阅前的预检:把 URL 抓下来,判断格式并统计节点类型,交给前端判可用性。
|
||
# 不能交给内核去做——内核只在保存后才拉,失败了用户只能翻日志。
|
||
probe_subscription() {
|
||
request=$(cat)
|
||
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
|
||
json_get_var url url
|
||
case "$url" in
|
||
http://*|https://*) ;;
|
||
*) emit_result false subscription_invalid; return 1 ;;
|
||
esac
|
||
# 只允许普通的 URL 字符,避免把奇怪的参数塞进 curl
|
||
case "$url" in
|
||
*[!A-Za-z0-9._~:/?#@\!\$\&\(\)\*+,\;=%-]*) emit_result false subscription_invalid; return 1 ;;
|
||
esac
|
||
|
||
probe_file=$(mktemp /tmp/honk-probe.XXXXXX) || { emit_result false probe_failed; return 1; }
|
||
chmod 0600 "$probe_file" || { rm -f "$probe_file"; emit_result false probe_failed; return 1; }
|
||
if ! curl -fsSL --max-time 25 --connect-timeout 8 -A 'clash.meta/1.19.30' -o "$probe_file" "$url" 2>/dev/null; then
|
||
if ! curl -fsSL --max-time 25 -o "$probe_file" "$url" 2>/dev/null; then
|
||
rm -f "$probe_file"
|
||
emit_result false subscription_unreachable
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
probe_bytes=$(wc -c < "$probe_file" 2>/dev/null | tr -d ' ')
|
||
case "$probe_bytes" in ''|*[!0-9]*) probe_bytes=0 ;; esac
|
||
if [ "$probe_bytes" -eq 0 ]; then
|
||
rm -f "$probe_file"
|
||
emit_result false subscription_empty
|
||
return 1
|
||
fi
|
||
|
||
# 超过 4 MiB 直接拒绝:订阅不该这么大,继续读只会拖垮路由器
|
||
if [ "$probe_bytes" -gt 4194304 ]; then
|
||
rm -f "$probe_file"
|
||
emit_result false subscription_too_large
|
||
return 1
|
||
fi
|
||
|
||
probe_format=unknown
|
||
probe_types=
|
||
if grep -qE '^proxies:[[:space:]]*$' "$probe_file" 2>/dev/null; then
|
||
probe_format=clash
|
||
probe_types=$(awk '
|
||
/^proxies:[[:space:]]*$/ { inside = 1; next }
|
||
inside && /^[A-Za-z0-9_-]+:/ { inside = 0 }
|
||
inside {
|
||
if (match($0, /type:[[:space:]]*["'"'"']?[A-Za-z0-9_-]+/)) {
|
||
t = substr($0, RSTART, RLENGTH)
|
||
sub(/^type:[[:space:]]*["'"'"']?/, "", t)
|
||
print t
|
||
}
|
||
}
|
||
' "$probe_file" | sort | uniq -c | awk '{ printf "%s:%s ", $2, $1 }')
|
||
elif head -c 1 "$probe_file" 2>/dev/null | grep -qE '[\{\[]'; then
|
||
probe_format=json
|
||
probe_types=$(grep -oE '"type"[[:space:]]*:[[:space:]]*"[A-Za-z0-9_-]+"' "$probe_file" 2>/dev/null \
|
||
| sed 's/.*"\([A-Za-z0-9_-]*\)"$/\1/' | sort | uniq -c | awk '{ printf "%s:%s ", $2, $1 }')
|
||
elif grep -qE '^\[[A-Za-z ]+\]' "$probe_file" 2>/dev/null; then
|
||
probe_format=records
|
||
probe_types=$(sed -n 's/^[^=][^=]*=[[:space:]]*\([A-Za-z0-9_-]*\),.*/\1/p' "$probe_file" 2>/dev/null \
|
||
| sort | uniq -c | awk '{ printf "%s:%s ", $2, $1 }')
|
||
elif grep -qE '://' "$probe_file" 2>/dev/null; then
|
||
probe_format=uri
|
||
probe_types=$(grep -oE '^[A-Za-z0-9+.-]+://' "$probe_file" 2>/dev/null \
|
||
| sed 's|://$||' | sort | uniq -c | awk '{ printf "%s:%s ", $2, $1 }')
|
||
elif tr -d '\r\n' < "$probe_file" | grep -qE '^[A-Za-z0-9+/=]+$'; then
|
||
# 整份是 base64:本机没有 base64 解码工具,把小样本交给前端解
|
||
probe_format=base64
|
||
fi
|
||
|
||
json_init
|
||
json_add_boolean ok 1
|
||
json_add_string message probe_ok
|
||
json_add_string format "$probe_format"
|
||
json_add_number bytes "$probe_bytes"
|
||
json_add_string types "$probe_types"
|
||
if [ "$probe_format" = base64 ] && [ "$probe_bytes" -le 262144 ]; then
|
||
probe_body=$(cat "$probe_file")
|
||
json_add_string content "$probe_body"
|
||
fi
|
||
rm -f "$probe_file"
|
||
json_dump
|
||
}
|
||
|
||
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
|
||
|
||
# 名字允许中文和空格:内核的 tag 支持引号写法,我们只在写入时决定要不要加引号。
|
||
# 但单引号、反斜杠、换行会破坏 dae 的字符串,必须拒绝。
|
||
case "$sub_name" in
|
||
'') emit_result false subscription_invalid; return 1 ;;
|
||
*\'*|*\\*) emit_result false subscription_invalid; return 1 ;;
|
||
esac
|
||
[ "$(printf '%s' "$sub_name" | tr -d '\n\r')" = "$sub_name" ] || { emit_result false subscription_invalid; return 1; }
|
||
[ "$(printf '%s' "$sub_name" | wc -c)" -le 64 ] || { emit_result false subscription_invalid; return 1; }
|
||
case "$sub_name" in
|
||
*[!A-Za-z0-9_-]*) sub_tag="'$sub_name'" ;;
|
||
*) sub_tag=$sub_name ;;
|
||
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
|
||
# 快捷追加只支持系统托管结构;自带 global/experimental 或 include 其他文件一律拒绝。
|
||
[ "$(config_owner "$cfg")" = system ] || { emit_result false quick_add_unsupported; return 1; }
|
||
original_hash=$(sha256sum "$cfg" | awk '{print $1}')
|
||
|
||
if grep -qF "$sub_tag:" "$cfg" || grep -qF "'$sub_name':" "$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_tag" -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
|
||
# 快捷追加只支持系统托管结构;自带 global/experimental 或 include 其他文件一律拒绝。
|
||
[ "$(config_owner "$cfg")" = system ] || { emit_result false quick_add_unsupported; return 1; }
|
||
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; }
|
||
begin_mutation
|
||
|
||
# 提取 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
|
||
# A caller that timed out mid-operation cannot release the lock itself. Without
|
||
# this, one aborted apply wedges every later configuration operation.
|
||
if honk_lock_is_stale; then rm -rf "$HONK_LOCK_DIR"; 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
|
||
}
|
||
|
||
# 忘记密码:清掉核心里的管理员记录,并让 LuCI 重新显示首次设置。
|
||
# 只删管理员,不动订阅/分组/路由;需要先停服务(内核自己也会拒绝运行中的 reset)。
|
||
reset_admin() {
|
||
begin_mutation
|
||
if pidof honk-core >/dev/null 2>&1; then
|
||
honk_lifecycle_stop 30 || { emit_result false service_stop_failed_rolled_back; return 1; }
|
||
fi
|
||
reset_data_dir=$(sed -n "s/^[[:space:]]*data_dir:[[:space:]]*'\([^']*\)'.*/\1/p" /etc/honk/system.dae 2>/dev/null | head -n 1)
|
||
[ -n "$reset_data_dir" ] || reset_data_dir=/etc/honk
|
||
case "$reset_data_dir" in /*) ;; *) reset_data_dir=/etc/honk ;; esac
|
||
if [ -f "$reset_data_dir/state/honk.db" ]; then
|
||
reset_backup=$(mktemp /etc/honk/.state-backup.XXXXXX) || { emit_result false config_write_failed; return 1; }
|
||
cp -p "$reset_data_dir/state/honk.db" "$reset_backup" || { rm -f "$reset_backup"; emit_result false config_write_failed; return 1; }
|
||
fi
|
||
if ! reset_output=$("$CORE" --config /etc/honk/config.dae --data-dir "$reset_data_dir" admin reset 2>&1); then
|
||
[ -n "${reset_backup:-}" ] && rm -f "$reset_backup"
|
||
logger -t honk "administrator reset failed: $reset_output" 2>/dev/null || :
|
||
emit_result false administrator_reset_failed
|
||
return 1
|
||
fi
|
||
if ! uci set honk.main.initialized=0 || ! uci set honk.main.enabled=0 || ! uci commit honk || ! chmod 0600 /etc/config/honk; then
|
||
emit_result false config_write_failed
|
||
return 1
|
||
fi
|
||
rm -f "${reset_backup:-}"
|
||
emit_result true administrator_reset
|
||
}
|
||
|
||
config_read() {
|
||
[ -f /etc/honk/config.dae ] && [ ! -L /etc/honk/config.dae ] || { emit_result false config_missing; return 1; }
|
||
config_size=$(wc -c < /etc/honk/config.dae)
|
||
[ "$config_size" -le "$DAE_UPLOAD_LIMIT" ] || { emit_result false config_too_large; return 1; }
|
||
config_content=$(cat /etc/honk/config.dae; printf '\001') || { emit_result false config_read_failed; return 1; }
|
||
config_content=${config_content%?}
|
||
json_init
|
||
json_add_boolean ok 1
|
||
json_add_string message config_loaded
|
||
json_add_string content "$config_content"
|
||
json_dump
|
||
}
|
||
|
||
# 只读草稿,不触发任何应用。source=legacy 时读旧 manual-config.dae(用户明确选择才调)。
|
||
config_draft_read() {
|
||
request=$(cat)
|
||
draft_source=draft
|
||
if [ -n "$request" ]; then
|
||
json_load "$request" 2>/dev/null || { emit_result false invalid_request; return 1; }
|
||
json_get_var draft_source source
|
||
fi
|
||
case "$draft_source" in
|
||
draft) draft_file=/etc/honk/config-draft.dae ;;
|
||
legacy) draft_file=/etc/honk/manual-config.dae ;;
|
||
*) emit_result false invalid_request; return 1 ;;
|
||
esac
|
||
[ -f "$draft_file" ] && [ ! -L "$draft_file" ] || { emit_result false config_draft_missing; return 1; }
|
||
config_size=$(wc -c < "$draft_file")
|
||
[ "$config_size" -le "$DAE_UPLOAD_LIMIT" ] || { emit_result false config_too_large; return 1; }
|
||
config_content=$(cat "$draft_file"; printf '\001') || { emit_result false config_read_failed; return 1; }
|
||
config_content=${config_content%?}
|
||
json_init
|
||
json_add_boolean ok 1
|
||
json_add_string message config_draft_loaded
|
||
json_add_string source "$draft_source"
|
||
json_add_string content "$config_content"
|
||
json_dump
|
||
}
|
||
|
||
lan_candidates() {
|
||
lan_dump=$(ubus call network.interface dump 2>/dev/null) || { emit_result false lan_candidates_unavailable; return 1; }
|
||
lan_file=$(mktemp /tmp/honk-lan-candidates.XXXXXX) || { emit_result false lan_candidates_unavailable; return 1; }
|
||
chmod 0600 "$lan_file" || { rm -f "$lan_file"; emit_result false lan_candidates_unavailable; return 1; }
|
||
printf '%s' "$lan_dump" > "$lan_file" || { rm -f "$lan_file"; emit_result false lan_candidates_unavailable; return 1; }
|
||
json_init
|
||
json_add_boolean ok 1
|
||
json_add_string message lan_candidates_loaded
|
||
json_add_array candidates
|
||
for lan_name in $(jsonfilter -i "$lan_file" -e '@.interface[*].interface' 2>/dev/null); do
|
||
case "$lan_name" in ''|*[!A-Za-z0-9_.-]*) continue ;; esac
|
||
lan_status=$(ubus call "network.interface.$lan_name" status 2>/dev/null) || continue
|
||
lan_up=$(printf '%s' "$lan_status" | jsonfilter -e '@.up' 2>/dev/null)
|
||
[ "$lan_up" = true ] || continue
|
||
lan_device=$(printf '%s' "$lan_status" | jsonfilter -e '@.device' 2>/dev/null)
|
||
lan_l3=$(printf '%s' "$lan_status" | jsonfilter -e '@.l3_device' 2>/dev/null)
|
||
[ -n "$lan_l3" ] || lan_l3=$lan_device
|
||
case "$lan_l3:$lan_device" in *[!A-Za-z0-9_.:-]*) continue ;; esac
|
||
[ -n "$lan_l3" ] || continue
|
||
lan_ip=$(printf '%s' "$lan_status" | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null)
|
||
json_add_object ""
|
||
json_add_string network "$lan_name"
|
||
json_add_string l3_device "$lan_l3"
|
||
json_add_string device "$lan_device"
|
||
json_add_string ipv4 "$lan_ip"
|
||
json_close_object
|
||
done
|
||
json_close_array
|
||
json_dump
|
||
rm -f "$lan_file"
|
||
}
|
||
|
||
|
||
config_candidate_from_request() {
|
||
request=$(cat)
|
||
json_load "$request" 2>/dev/null || return 1
|
||
json_get_var config_content content
|
||
config_bytes=$(printf '%s' "$config_content" | wc -c)
|
||
case "$config_bytes" in ''|*[!0-9]*) return 1 ;; esac
|
||
[ "$config_bytes" -le "$DAE_UPLOAD_LIMIT" ] || return 2
|
||
umask 077
|
||
# Use mktemp -d for BusyBox compatibility, then create an exclusive .dae
|
||
# sibling so relative paths resolve from the same directory as config.dae.
|
||
config_candidate_dir=$(mktemp -d /etc/honk/.config-candidate.XXXXXX) || return 3
|
||
config_candidate=/etc/honk/.config-candidate-${config_candidate_dir##*.config-candidate.}.dae
|
||
(set -C; printf '%s' "$config_content" > "$config_candidate") || { rm -rf "$config_candidate_dir"; return 3; }
|
||
rmdir "$config_candidate_dir" || { rm -f "$config_candidate"; rm -rf "$config_candidate_dir"; return 3; }
|
||
chmod 0600 "$config_candidate" || { rm -f "$config_candidate"; return 3; }
|
||
return 0
|
||
}
|
||
|
||
validate_config_candidate() {
|
||
validation_file=$(mktemp /tmp/honk-config-validation.XXXXXX) || return 1
|
||
chmod 0600 "$validation_file" || { rm -f "$validation_file"; return 1; }
|
||
if "$CORE" config validate --config "$config_candidate" > "$validation_file" 2>/dev/null; then
|
||
config_validation=$(cat "$validation_file")
|
||
json_load "$config_validation" 2>/dev/null || { rm -f "$validation_file"; return 1; }
|
||
json_get_var validation_ok ok
|
||
rm -f "$validation_file"
|
||
[ "$validation_ok" = 1 ] || return 1
|
||
return 0
|
||
fi
|
||
config_validation=$(cat "$validation_file")
|
||
json_load "$config_validation" 2>/dev/null || config_validation='{"ok":false,"message":"configuration_validation_failed","diagnostics":[]}'
|
||
rm -f "$validation_file"
|
||
return 1
|
||
}
|
||
|
||
config_validate() {
|
||
config_candidate_from_request
|
||
case "$?" in
|
||
1) emit_result false config_request_invalid; return 1 ;;
|
||
2) emit_result false config_too_large; return 1 ;;
|
||
3) emit_result false config_write_failed; return 1 ;;
|
||
esac
|
||
validate_config_candidate
|
||
validation_status=$?
|
||
rm -f "$config_candidate"
|
||
if [ -n "$config_validation" ]; then
|
||
printf '%s\n' "$config_validation"
|
||
return 0
|
||
fi
|
||
emit_result false config_validation_unavailable
|
||
return 1
|
||
}
|
||
|
||
config_save() {
|
||
begin_mutation
|
||
config_candidate_from_request
|
||
case "$?" in
|
||
1) emit_result false config_request_invalid; return 1 ;;
|
||
2) emit_result false config_too_large; return 1 ;;
|
||
3) emit_result false config_write_failed; return 1 ;;
|
||
esac
|
||
if ! validate_config_candidate; then
|
||
rm -f "$config_candidate"
|
||
[ -n "$config_validation" ] && printf '%s\n' "$config_validation" || emit_result false config_validation_unavailable
|
||
return 0
|
||
fi
|
||
draft_path=/etc/honk/config-draft.dae
|
||
draft_backup=
|
||
if [ -e "$draft_path" ] || [ -L "$draft_path" ]; then
|
||
[ -f "$draft_path" ] && [ ! -L "$draft_path" ] || { rm -f "$config_candidate"; emit_result false config_draft_invalid; return 1; }
|
||
draft_backup=$(mktemp /etc/honk/.config-draft-backup.XXXXXX) || { rm -f "$config_candidate"; emit_result false config_write_failed; return 1; }
|
||
cp -p "$draft_path" "$draft_backup" || { rm -f "$config_candidate" "$draft_backup"; emit_result false config_write_failed; return 1; }
|
||
fi
|
||
if ! mv -f "$config_candidate" "$draft_path"; then
|
||
if [ -n "$draft_backup" ]; then
|
||
draft_restore=$(mktemp /etc/honk/.config-draft-restore.XXXXXX)
|
||
cp -p "$draft_backup" "$draft_restore" && mv -f "$draft_restore" "$draft_path"
|
||
rm -f "$draft_restore"
|
||
fi
|
||
rm -f "$draft_backup"
|
||
emit_result false config_write_failed
|
||
return 1
|
||
fi
|
||
chmod 0600 "$draft_path" || { emit_result false config_write_failed; return 1; }
|
||
rm -f "$draft_backup"
|
||
emit_result true config_saved_pending_apply
|
||
}
|
||
|
||
config_apply() {
|
||
begin_mutation
|
||
draft_path=/etc/honk/config-draft.dae
|
||
[ -f "$draft_path" ] && [ ! -L "$draft_path" ] || { emit_result false config_draft_missing; return 1; }
|
||
activate_dir=$(mktemp -d /etc/honk/.config-activate.XXXXXX) || { emit_result false config_write_failed; return 1; }
|
||
activate_file=/etc/honk/.config-${activate_dir##*.config-activate.}.dae
|
||
(set -C; cp "$draft_path" "$activate_file") || { rm -rf "$activate_dir"; emit_result false config_write_failed; return 1; }
|
||
rmdir "$activate_dir" || { rm -rf "$activate_dir"; rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||
chmod 0600 "$activate_file" || { rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||
config_candidate=$activate_file
|
||
if ! validate_config_candidate; then
|
||
rm -f "$activate_file"
|
||
[ -n "$config_validation" ] && printf '%s\n' "$config_validation" || emit_result false config_validation_unavailable
|
||
return 1
|
||
fi
|
||
candidate_hash=$(sha256sum "$activate_file" | awk '{print $1}')
|
||
old_hash=$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')
|
||
old_running=0
|
||
old_pid=$(honk_lifecycle_managed_pid 2>/dev/null)
|
||
if [ -n "$old_pid" ]; then
|
||
old_running=1
|
||
elif pidof honk-core >/dev/null 2>&1; then
|
||
rm -f "$activate_file"
|
||
emit_result false unmanaged_core_running
|
||
return 1
|
||
fi
|
||
backup_dir=$(mktemp -d /etc/honk/.config-apply-backup.XXXXXX) || { rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||
chmod 0700 "$backup_dir" || { rm -rf "$backup_dir"; rm -f "$activate_file"; emit_result false config_write_failed; return 1; }
|
||
cp -p /etc/honk/config.dae "$backup_dir/config.dae" && cp -p /etc/config/honk "$backup_dir/honk.uci" || {
|
||
rm -rf "$backup_dir" "$activate_file"
|
||
emit_result false config_write_failed
|
||
return 1
|
||
}
|
||
if [ "$old_running" = 1 ] && ! honk_lifecycle_stop 30; then
|
||
rm -f "$activate_file"
|
||
emit_result false service_stop_unconfirmed
|
||
return 1
|
||
fi
|
||
if [ "$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')" != "$old_hash" ]; then
|
||
if [ "$old_running" = 1 ]; then honk_lifecycle_start 20 || { emit_result false config_changed_concurrently_restart_failed; return 1; }; fi
|
||
rm -rf "$backup_dir" "$activate_file"
|
||
emit_result false config_changed_concurrently
|
||
return 1
|
||
fi
|
||
if ! mv -f "$activate_file" /etc/honk/config.dae; then
|
||
emit_result false config_write_failed
|
||
return 1
|
||
fi
|
||
if ! uci set honk.main.enabled=1 || ! uci set honk.main.initialized=1 || ! uci commit honk || ! chmod 0600 /etc/config/honk; then
|
||
apply_failed=1
|
||
else
|
||
apply_failed=0
|
||
fi
|
||
if [ "$apply_failed" = 0 ] && honk_lifecycle_start 20; then
|
||
rm -rf "$backup_dir"
|
||
emit_result true config_applied
|
||
return 0
|
||
fi
|
||
[ "$apply_failed" = 1 ] || honk_lifecycle_stop 30 >/dev/null 2>&1 || :
|
||
rollback_ok=1
|
||
if [ "$(sha256sum /etc/honk/config.dae 2>/dev/null | awk '{print $1}')" = "$candidate_hash" ]; then
|
||
rollback_tmp=$(mktemp /etc/honk/.config-rollback.XXXXXX) || rollback_ok=0
|
||
[ "$rollback_ok" = 0 ] || { cp -p "$backup_dir/config.dae" "$rollback_tmp" && mv -f "$rollback_tmp" /etc/honk/config.dae || rollback_ok=0; }
|
||
else
|
||
rollback_ok=0
|
||
fi
|
||
uci_tmp=$(mktemp /etc/config/.honk-rollback.XXXXXX) || rollback_ok=0
|
||
[ "$rollback_ok" = 0 ] || { cp -p "$backup_dir/honk.uci" "$uci_tmp" && mv -f "$uci_tmp" /etc/config/honk || rollback_ok=0; }
|
||
[ "$rollback_ok" = 0 ] || [ "$(sha256sum /etc/config/honk 2>/dev/null | awk '{print $1}')" = "$(sha256sum "$backup_dir/honk.uci" | awk '{print $1}')" ] || rollback_ok=0
|
||
uci revert honk 2>/dev/null || :
|
||
if [ "$rollback_ok" = 1 ] && [ "$old_running" = 1 ]; then honk_lifecycle_start 10 || rollback_ok=0; fi
|
||
rm -f "$rollback_tmp" "$uci_tmp"
|
||
if [ "$rollback_ok" = 1 ]; then
|
||
rm -rf "$backup_dir"
|
||
emit_result false config_apply_failed_rolled_back
|
||
else
|
||
emit_result false config_apply_rollback_failed
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
|
||
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"},"probe_subscription":{"url":"String"},"reset_admin":{},"add_nodes":{"links":"String"},"import_dae":{},"config_read":{},"config_draft_read":{},"lan_candidates":{},"config_validate":{"content":"String"},"config_save":{"content":"String"},"config_apply":{},"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
|
||
;;
|
||
probe_subscription)
|
||
probe_subscription
|
||
;;
|
||
reset_admin)
|
||
reset_admin
|
||
;;
|
||
add_subscription)
|
||
add_subscription
|
||
;;
|
||
add_nodes)
|
||
add_nodes
|
||
;;
|
||
import_dae)
|
||
import_dae
|
||
;;
|
||
config_read)
|
||
config_read
|
||
;;
|
||
config_draft_read)
|
||
config_draft_read
|
||
;;
|
||
lan_candidates)
|
||
lan_candidates
|
||
;;
|
||
config_validate)
|
||
config_validate
|
||
;;
|
||
config_save)
|
||
config_save
|
||
;;
|
||
config_apply)
|
||
config_apply
|
||
;;
|
||
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_runtime_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
|