mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-02 05:33:56 +08:00
update 2026-10-29 01:36:37
This commit is contained in:
1 parent
3a91d7a1df
commit
36d6943a0b
287 files changed
+39005
-11853
No files matched your search
+199
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user