update 2026-10-29 18:46:39

This commit is contained in:
action committed 2026-10-29 18:46:39 +08:00
1 parent e2f86e09f4
commit 95f76f26d3
5034 files changed
+2043234

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
#!/bin/sh
# Download and install Dashboard for HONK
TARGET_DIR="${1:-}"
DOWNLOAD_URL="${2:-}"
TARGET_TYPE="${3:-}"
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> [target-type]"
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..."
IS_LITE=0
case "$TARGET_TYPE" in
*lite*) IS_LITE=1 ;;
esac
case "$TARGET_DIR" in
*lite*) IS_LITE=1 ;;
esac
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"
TAG_NO_V="${LATEST_TAG#[vV]}"
ASSETS_HTML=$(fetch_text "https://github.com/Zakkaus/doona/releases/expanded_assets/$LATEST_TAG")
if [ "$IS_LITE" -eq 1 ]; then
ASSET_PATH=$(printf "%s" "$ASSETS_HTML" | grep -o '/Zakkaus/doona/releases/download/[^"]*/doona-lite-[^"]*\.tar\.gz' | head -n 1)
if [ -n "$ASSET_PATH" ]; then
RESOLVED_URL="https://github.com${ASSET_PATH}"
log "Found package: $ASSET_PATH"
else
RESOLVED_URL="https://github.com/Zakkaus/doona/releases/download/${LATEST_TAG}/doona-lite-${TAG_NO_V}.tar.gz"
log "Inferred package URL: $RESOLVED_URL"
fi
else
ASSET_PATH=$(printf "%s" "$ASSETS_HTML" | grep -o '/Zakkaus/doona/releases/download/[^"]*/doona-[^"]*\.tar\.gz' | grep -v 'fonts' | grep -v 'lite' | grep -v 'precompressed' | head -n 1)
if [ -n "$ASSET_PATH" ]; then
RESOLVED_URL="https://github.com${ASSET_PATH}"
log "Found package: $ASSET_PATH"
else
RESOLVED_URL="https://github.com/Zakkaus/doona/releases/download/${LATEST_TAG}/doona-${TAG_NO_V}.tar.gz"
log "Inferred package URL: $RESOLVED_URL"
fi
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
@@ -0,0 +1,69 @@
{
"admin/services/honk": {
"title": "HONK",
"order": -1,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-honk" ],
"uci": { "honk": true }
}
},
"admin/services/honk/global": {
"title": "Global Settings",
"order": 1,
"action": {
"type": "view",
"path": "honk/global"
}
},
"admin/services/honk/dns": {
"title": "DNS Settings",
"order": 2,
"action": {
"type": "view",
"path": "honk/dns"
}
},
"admin/services/honk/node": {
"title": "Node Settings",
"order": 3,
"action": {
"type": "view",
"path": "honk/node"
}
},
"admin/services/honk/route": {
"title": "Routing Settings",
"order": 4,
"action": {
"type": "view",
"path": "honk/route"
}
},
"admin/services/honk/api": {
"title": "API Settings",
"order": 5,
"action": {
"type": "view",
"path": "honk/api"
}
},
"admin/services/honk/dashboard": {
"title": "Dashboard",
"order": 6,
"action": {
"type": "view",
"path": "honk/dashboard"
}
},
"admin/services/honk/log": {
"title": "Logs",
"order": 7,
"action": {
"type": "view",
"path": "honk/log"
}
}
}
@@ -0,0 +1,31 @@
{
"luci-app-honk": {
"description": "Grant access for luci-app-honk",
"read": {
"ubus": {
"luci.honk": [ "*" ]
},
"uci": [ "honk" ],
"file": [
"/etc/honk/config.dae",
"/etc/honk/config.d/*",
"/etc/honk/doona/*",
"/etc/honk/doona-lite/*",
"/etc/honk/zashboard/*"
]
},
"write": {
"ubus": {
"luci.honk": [ "*" ]
},
"uci": [ "honk" ],
"file": [
"/etc/honk/config.dae",
"/etc/honk/config.d/*",
"/etc/honk/doona/*",
"/etc/honk/doona-lite/*",
"/etc/honk/zashboard/*"
]
}
}
}
@@ -0,0 +1,619 @@
#!/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",
doona_lite: "/etc/honk/doona-lite"
};
function normalize_dashboard_type(type) {
if (type == "doona-lite")
return "doona_lite";
return type;
}
function get_dashboard_dir(type) {
type = normalize_dashboard_type(type);
return DASHBOARD_DIRS[type] || DASHBOARD_DIRS.doona;
}
function is_other_default_ui(ui_path, current_type) {
current_type = normalize_dashboard_type(current_type);
for (let t, dir in DASHBOARD_DIRS) {
if (t != current_type && dir == ui_path)
return true;
}
return false;
}
function is_valid_dashboard_type(type) {
if (type == "none")
return true;
type = normalize_dashboard_type(type);
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);
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 && !is_other_default_ui(parsed_native.ui, requested_type)) ? 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 && !is_other_default_ui(parsed_native.ui, requested_type)) {
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";
}
requested_type = normalize_dashboard_type(requested_type);
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 target_dir = get_dashboard_dir(requested_type);
if (parsed_native && parsed_native.ui && !is_other_default_ui(parsed_native.ui, requested_type) &&
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 safe_type = replace(requested_type, "'", "'\\''");
let cmd = sprintf("/bin/sh '%s' '%s' '%s' '%s' >/dev/null 2>&1 &", safe_script, safe_target, safe_url, safe_type);
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;
target_type = normalize_dashboard_type(target_type);
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);
}
}
}
};