Files
2026-10-08 13:17:49 +08:00

2070 lines
59 KiB
Bash
Executable File

#!/bin/sh
set -eu
UCI_PACKAGE="oxidns"
UCI_SECTION="main"
OXIDNS_BIN="/usr/bin/oxidns"
WEBUI_DIR="/usr/share/oxidns/webui"
TARGETS_FILE="${OXIDNS_TARGETS_FILE:-/usr/share/oxidns/targets.json}"
DEFAULT_CONFIG_PATH="/etc/oxidns/config.yaml"
DEFAULT_WORKING_DIR="/var/lib/oxidns"
DEFAULT_SERVICE_NAME="oxidns"
DEFAULT_CORE_REPOSITORY="svenshi/oxidns"
DEFAULT_CORE_BUNDLE="full"
GITHUB_API_BASE="https://api.github.com"
TMP_ROOT="${TMPDIR:-/tmp}"
CORE_PROGRESS_DIR="$TMP_ROOT/luci-oxidns"
CORE_PROGRESS_LOG="$CORE_PROGRESS_DIR/core-progress.log"
CORE_LOCK_DIR="$CORE_PROGRESS_DIR/core-operation.lock"
CORE_LOCK_HELD=0
CONFIG_PATH="$DEFAULT_CONFIG_PATH"
WORKING_DIR="$DEFAULT_WORKING_DIR"
SERVICE_NAME="$DEFAULT_SERVICE_NAME"
CORE_REPOSITORY="$DEFAULT_CORE_REPOSITORY"
CORE_BUNDLE="$DEFAULT_CORE_BUNDLE"
LOG_SYSLOG=1
DOWNLOAD_PROXY=""
GITHUB_TOKEN=""
DOWNLOAD_ERROR=""
DOWNLOAD_RC=""
json_escape_stream() {
awk 'BEGIN { ORS = "" }
{
if (NR > 1)
printf "\\n";
for (i = 1; i <= length($0); i++) {
c = substr($0, i, 1);
if (c == "\\")
printf "\\\\";
else if (c == "\"")
printf "\\\"";
else if (c == "\t")
printf "\\t";
else if (c == "\r")
printf "\\r";
else
printf "%s", c;
}
}'
}
json_string() {
printf '"'
printf '%s' "$1" | json_escape_stream
printf '"'
}
json_file_string() {
printf '"'
if [ -r "$1" ]; then
json_escape_stream < "$1"
fi
printf '"'
}
json_bool() {
if [ "${1:-0}" = "1" ]; then
printf 'true'
else
printf 'false'
fi
}
json_error() {
printf '{'
printf '"ok":false,'
printf '"code":'
json_string "$1"
printf ',"message":'
json_string "$2"
printf '}\n'
}
json_detailed_error() {
CODE="$1"
MESSAGE="$2"
STAGE="${3:-}"
DETAIL="${4:-}"
printf '{'
printf '"ok":false,'
printf '"code":'
json_string "$CODE"
printf ',"message":'
json_string "$MESSAGE"
if [ -n "$STAGE" ]; then
printf ',"stage":'
json_string "$STAGE"
fi
if [ -n "$DETAIL" ]; then
printf ',"detail":'
json_string "$DETAIL"
fi
printf '}\n'
}
sanitize_progress_text() {
awk -v token="$GITHUB_TOKEN" -v proxy="$DOWNLOAD_PROXY" '
function redact(value, secret, out, pos) {
if (secret == "")
return value;
out = "";
while ((pos = index(value, secret)) > 0) {
out = out substr(value, 1, pos - 1) "[redacted]";
value = substr(value, pos + length(secret));
}
return out value;
}
{
line = redact(redact($0, token), proxy);
gsub(/[[:cntrl:]]/, " ", line);
print line;
}
'
}
prepare_core_progress_dir() {
if [ -L "$CORE_PROGRESS_DIR" ] || { [ -e "$CORE_PROGRESS_DIR" ] && [ ! -d "$CORE_PROGRESS_DIR" ]; }; then
return 1
fi
if [ ! -d "$CORE_PROGRESS_DIR" ]; then
mkdir "$CORE_PROGRESS_DIR" 2>/dev/null || return 1
fi
if command -v chown >/dev/null 2>&1; then
chown 0:0 "$CORE_PROGRESS_DIR" 2>/dev/null || true
fi
chmod 700 "$CORE_PROGRESS_DIR" 2>/dev/null || true
return 0
}
prepare_core_progress_file() {
RESET_FILE="${1:-0}"
prepare_core_progress_dir || return 1
if [ -L "$CORE_PROGRESS_LOG" ] || { [ -e "$CORE_PROGRESS_LOG" ] && [ ! -f "$CORE_PROGRESS_LOG" ]; }; then
rm -f "$CORE_PROGRESS_LOG" 2>/dev/null || return 1
fi
if [ "$RESET_FILE" = "1" ] || [ ! -e "$CORE_PROGRESS_LOG" ]; then
: > "$CORE_PROGRESS_LOG" 2>/dev/null || return 1
fi
[ ! -L "$CORE_PROGRESS_LOG" ] || return 1
if command -v chown >/dev/null 2>&1; then
chown 0:0 "$CORE_PROGRESS_LOG" 2>/dev/null || true
fi
chmod 600 "$CORE_PROGRESS_LOG" 2>/dev/null || true
return 0
}
progress_timestamp() {
date '+%H:%M:%S' 2>/dev/null || printf '--:--:--'
}
core_progress_log() {
PROGRESS_KIND="$1"
PROGRESS_MESSAGE="$2"
PROGRESS_TEXT="$(printf '%s' "$PROGRESS_MESSAGE" | sanitize_progress_text)"
[ -n "$PROGRESS_TEXT" ] || PROGRESS_TEXT="-"
prepare_core_progress_file 0 || return 0
printf '[%s] %s %s\n' "$(progress_timestamp)" "$PROGRESS_KIND" "$PROGRESS_TEXT" >> "$CORE_PROGRESS_LOG" 2>/dev/null || true
}
core_progress_command() {
core_progress_log "command" "+ $1"
}
core_progress_reset() {
prepare_core_progress_file 1 || return 0
core_progress_log "info" "$1"
}
pid_is_running() {
PID_VALUE="$1"
case "$PID_VALUE" in
''|*[!0-9]*)
return 1
;;
esac
kill -0 "$PID_VALUE" 2>/dev/null
}
clear_stale_core_lock() {
[ -d "$CORE_LOCK_DIR" ] && [ ! -L "$CORE_LOCK_DIR" ] || return 1
LOCK_PID="$(sed -n '1p' "$CORE_LOCK_DIR/pid" 2>/dev/null || true)"
if pid_is_running "$LOCK_PID"; then
return 1
fi
rm -f "$CORE_LOCK_DIR/pid" 2>/dev/null || return 1
rmdir "$CORE_LOCK_DIR" 2>/dev/null || return 1
}
acquire_core_lock() {
prepare_core_progress_dir || {
json_error "core_lock_failed" "failed to prepare core operation lock directory"
return 1
}
if mkdir "$CORE_LOCK_DIR" 2>/dev/null || { clear_stale_core_lock && mkdir "$CORE_LOCK_DIR" 2>/dev/null; }; then
CORE_LOCK_HELD=1
printf '%s\n' "$$" > "$CORE_LOCK_DIR/pid" 2>/dev/null || true
chmod 700 "$CORE_LOCK_DIR" 2>/dev/null || true
return 0
fi
core_progress_log "error" "another core operation is already running"
json_error "core_operation_busy" "another OxiDNS core operation is already running"
return 1
}
release_core_lock() {
if [ "${CORE_LOCK_HELD:-0}" = "1" ]; then
rm -f "$CORE_LOCK_DIR/pid" 2>/dev/null || true
rmdir "$CORE_LOCK_DIR" 2>/dev/null || true
CORE_LOCK_HELD=0
fi
}
run_with_core_lock() {
acquire_core_lock || return 1
trap 'release_core_lock' EXIT
trap 'release_core_lock; exit 1' HUP INT TERM
"$@"
CORE_ACTION_RC="$?"
release_core_lock
trap - EXIT HUP INT TERM
return "$CORE_ACTION_RC"
}
file_size_bytes() {
[ -f "$1" ] || {
printf '0'
return 0
}
wc -c < "$1" 2>/dev/null | awk '{ print $1 + 0 }'
}
emit_core_progress() {
if ! prepare_core_progress_file 0; then
printf '{"ok":true,"path":'
json_string "$CORE_PROGRESS_LOG"
printf ',"mtime":"","size":"0","text":""}\n'
return 0
fi
printf '{"ok":true,"path":'
json_string "$CORE_PROGRESS_LOG"
printf ',"mtime":'
json_string "$(stat_mtime "$CORE_PROGRESS_LOG")"
printf ',"size":'
json_string "$(file_size_bytes "$CORE_PROGRESS_LOG")"
printf ',"text":'
json_file_string "$CORE_PROGRESS_LOG"
printf '}\n'
}
uci_get() {
KEY="$1"
DEFAULT="$2"
if command -v uci >/dev/null 2>&1; then
uci -q get "$UCI_PACKAGE.$UCI_SECTION.$KEY" 2>/dev/null || printf '%s' "$DEFAULT"
else
printf '%s' "$DEFAULT"
fi
}
load_settings() {
SERVICE_NAME="$DEFAULT_SERVICE_NAME"
CONFIG_PATH="$(uci_get config_path "$DEFAULT_CONFIG_PATH")"
WORKING_DIR="$(uci_get working_dir "$DEFAULT_WORKING_DIR")"
CORE_REPOSITORY="$(uci_get core_repository "$DEFAULT_CORE_REPOSITORY")"
CORE_BUNDLE="$(uci_get core_bundle "$DEFAULT_CORE_BUNDLE")"
LOG_SYSLOG="$(uci_get log_syslog 1)"
case "$LOG_SYSLOG" in
0|off|false|no) LOG_SYSLOG=0 ;;
*) LOG_SYSLOG=1 ;;
esac
DOWNLOAD_PROXY="$(uci_get download_proxy '')"
GITHUB_TOKEN="$(uci_get github_token '')"
}
init_script() {
printf '/etc/init.d/%s' "$SERVICE_NAME"
}
service_enabled() {
INIT="$(init_script)"
[ -x "$INIT" ] && "$INIT" enabled >/dev/null 2>&1
}
service_running() {
INIT="$(init_script)"
if [ -x "$INIT" ] && "$INIT" status >/dev/null 2>&1; then
return 0
fi
if command -v pgrep >/dev/null 2>&1 && pgrep -x oxidns >/dev/null 2>&1; then
return 0
fi
return 1
}
yaml_scalar_clean() {
sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s/^"//; s/"$//; s/^'\''//; s/'\''$//'
}
config_api_listen() {
[ -r "$CONFIG_PATH" ] || return 1
awk '
/^[^[:space:]#][^:]*:/ {
top = $0;
sub(/:.*/, "", top);
second = "";
}
top == "api" && /^[[:space:]]+http:[[:space:]]*[^[:space:]#]/ {
sub(/^[[:space:]]+http:[[:space:]]*/, "", $0);
print $0;
exit;
}
top == "api" && /^[[:space:]]+http:[[:space:]]*$/ {
second = "http";
next;
}
top == "api" && second == "http" && /^[[:space:]]+listen:[[:space:]]*/ {
sub(/^[[:space:]]+listen:[[:space:]]*/, "", $0);
print $0;
exit;
}
' "$CONFIG_PATH" | yaml_scalar_clean
}
webui_url_from_config() {
LISTEN="$(config_api_listen || true)"
[ -n "$LISTEN" ] || return 1
LISTEN="${LISTEN#http://}"
LISTEN="${LISTEN#https://}"
case "$LISTEN" in
*:*) ;;
*) return 1 ;;
esac
HOST="${LISTEN%:*}"
PORT="${LISTEN##*:}"
case "$HOST" in
''|'0.0.0.0'|'::'|'[::]')
HOST="127.0.0.1"
;;
esac
[ -n "$PORT" ] || return 1
printf 'http://%s:%s/' "$HOST" "$PORT"
}
listen_host_from_config() {
LISTEN="$(config_api_listen || true)"
[ -n "$LISTEN" ] || return 1
LISTEN="${LISTEN#http://}"
LISTEN="${LISTEN#https://}"
case "$LISTEN" in
*:*) ;;
*) return 1 ;;
esac
printf '%s' "${LISTEN%:*}"
}
is_loopback_host() {
case "$1" in
localhost|127.*|'::1'|'[::1]')
return 0
;;
esac
return 1
}
is_wildcard_host() {
case "$1" in
''|'0.0.0.0'|'::'|'[::]')
return 0
;;
esac
return 1
}
build_info_json() {
if [ -x "$OXIDNS_BIN" ]; then
"$OXIDNS_BIN" build-info 2>/dev/null || true
fi
}
json_value_from_string() {
KEY="$1"
JSON="$2"
if command -v jsonfilter >/dev/null 2>&1; then
printf '%s' "$JSON" | jsonfilter -e "@.$KEY" 2>/dev/null || true
else
printf '%s' "$JSON" | sed -n "s/.*\"$KEY\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | sed -n '1p'
fi
}
detect_rust_target_from_file() {
TARGET_FILE="$1"
ARCH="$2"
[ -r "$TARGET_FILE" ] || return 1
awk -v arch="$ARCH" '
function field_value(line, value) {
value = line;
sub(/^.*:[[:space:]]*"/, "", value);
sub(/".*$/, "", value);
return value;
}
/"rust_target"[[:space:]]*:/ {
target = field_value($0);
next;
}
target != "" && (/"uname_arches"[[:space:]]*:/ || /"aliases"[[:space:]]*:/) {
line = $0;
gsub(/[\[\],"]/, " ", line);
n = split(line, parts, /[[:space:]]+/);
for (i = 1; i <= n; i++) {
if (parts[i] == arch) {
print target;
found = 1;
exit;
}
}
}
END { exit found ? 0 : 1 }
' "$TARGET_FILE"
}
detect_rust_target() {
ARCH="$(uname -m 2>/dev/null || printf unknown)"
if detect_rust_target_from_file "$TARGETS_FILE" "$ARCH"; then
return 0
fi
case "$ARCH" in
x86_64|amd64)
printf 'x86_64-unknown-linux-musl'
;;
aarch64|arm64)
printf 'aarch64-unknown-linux-musl'
;;
i386|i686)
printf 'i686-unknown-linux-musl'
;;
armv7l|armv7)
printf 'armv7-unknown-linux-musleabihf'
;;
armv6l|armv6)
printf 'arm-unknown-linux-musleabihf'
;;
*)
return 1
;;
esac
}
emit_status() {
load_settings
ENABLED=0
RUNNING=0
CORE_INSTALLED=0
WEBUI_INSTALLED=0
WEBUI_LOCAL_ONLY=0
WEBUI_WILDCARD=0
BUILD_JSON="$(build_info_json)"
CORE_VERSION="$(json_value_from_string version "$BUILD_JSON")"
CORE_BUNDLE_VALUE="$(json_value_from_string bundle "$BUILD_JSON")"
CORE_TARGET="$(detect_rust_target 2>/dev/null || printf unknown)"
WEBUI_URL="$(webui_url_from_config 2>/dev/null || true)"
WEBUI_LISTEN="$(config_api_listen 2>/dev/null || true)"
WEBUI_HOST="$(listen_host_from_config 2>/dev/null || true)"
if [ -x "$OXIDNS_BIN" ]; then CORE_INSTALLED=1; fi
if [ -d "$WEBUI_DIR" ]; then WEBUI_INSTALLED=1; fi
if is_loopback_host "$WEBUI_HOST"; then WEBUI_LOCAL_ONLY=1; fi
if is_wildcard_host "$WEBUI_HOST"; then WEBUI_WILDCARD=1; fi
if service_enabled; then ENABLED=1; fi
if service_running; then RUNNING=1; fi
printf '{'
printf '"ok":true,'
printf '"backend":"rpcd",'
printf '"core":{"installed":'
json_bool "$CORE_INSTALLED"
printf ',"version":'
json_string "$CORE_VERSION"
printf ',"bundle":'
json_string "$CORE_BUNDLE_VALUE"
printf ',"target":'
json_string "$CORE_TARGET"
printf ',"binary_path":'
json_string "$OXIDNS_BIN"
printf '}'
printf ',"service":'
json_string "$SERVICE_NAME"
printf ',"config_path":'
json_string "$CONFIG_PATH"
printf ',"working_dir":'
json_string "$WORKING_DIR"
printf ',"service_status":{"running":'
json_bool "$RUNNING"
printf ',"enabled":'
json_bool "$ENABLED"
printf ',"init_script":'
json_string "$(init_script)"
printf '}'
printf ',"webui":{"installed":'
json_bool "$WEBUI_INSTALLED"
printf ',"url":'
json_string "$WEBUI_URL"
printf ',"listen":'
json_string "$WEBUI_LISTEN"
printf ',"local_only":'
json_bool "$WEBUI_LOCAL_ONLY"
printf ',"wildcard":'
json_bool "$WEBUI_WILDCARD"
printf ',"path":'
json_string "$WEBUI_DIR"
printf '}'
printf ',"build":'
if printf '%s' "$BUILD_JSON" | grep -q '^{'; then
printf '%s' "$BUILD_JSON"
else
printf 'null'
fi
printf '}\n'
}
syslog_service_event() {
command -v logger >/dev/null 2>&1 || return 0
logger -t luci-app-oxidns "$1" >/dev/null 2>&1 || true
return 0
}
# procd stop is asynchronous. Starting before the old instance is removed
# reuses it, and procd's instance update retains its stdout/stderr settings.
stop_service_and_wait() {
local init_path="$1"
local stop_deadline service_json root_type service_type instances_type instances
if ! command -v ubus >/dev/null 2>&1 || ! command -v jsonfilter >/dev/null 2>&1; then
printf 'cannot wait for OxiDNS to stop: ubus and jsonfilter are required\n' >&2
return 1
fi
"$init_path" stop || return 1
stop_deadline=$(($(date +%s) + 10))
while :; do
if ! service_json="$(ubus -t 2 call service list '{"name":"oxidns"}' 2>/dev/null)" ||
! root_type="$(printf '%s' "$service_json" | jsonfilter -t '@' 2>/dev/null)"; then
printf 'failed to query OxiDNS service state while stopping\n' >&2
return 1
fi
if [ "$root_type" != object ]; then
printf 'invalid OxiDNS service state while stopping\n' >&2
return 1
fi
service_type="$(printf '%s' "$service_json" | jsonfilter -t '@.oxidns' 2>/dev/null || true)"
instances_type="$(printf '%s' "$service_json" | jsonfilter -t '@.oxidns.instances' 2>/dev/null || true)"
case "$service_type:$instances_type" in
:|object:|object:object) ;;
*) printf 'invalid OxiDNS service instance state while stopping\n' >&2; return 1 ;;
esac
# Missing service/instances and an empty instances object are all stopped.
# Keep waiting even for running:false while the old instance still exists.
instances="$(printf '%s' "$service_json" | jsonfilter -t '@.oxidns.instances[*]' 2>/dev/null || true)"
[ -n "$instances" ] || return 0
if [ "$(date +%s)" -ge "$stop_deadline" ]; then
printf 'timed out waiting for the old OxiDNS service instance to be removed\n' >&2
return 1
fi
sleep 1
done
}
run_init_action() {
case "$2" in
restart)
stop_service_and_wait "$1" && "$1" start
;;
stop)
stop_service_and_wait "$1"
;;
*)
"$1" "$2"
;;
esac
}
run_service() {
load_settings
ACTION="$1"
INIT="$(init_script)"
case "$ACTION" in
start|restart|enable)
if [ ! -x "$OXIDNS_BIN" ]; then
syslog_service_event "service $ACTION ignored: OxiDNS core is not installed"
json_error "core_not_installed" "Install the OxiDNS core before using service controls"
return 1
fi
;;
esac
if [ ! -x "$INIT" ]; then
syslog_service_event "service $ACTION failed: init script not found: $INIT"
json_error "service_unavailable" "init script not found: $INIT"
return 1
fi
SERVICE_OUT="$(mktemp "$TMP_ROOT/luci-oxidns-service.XXXXXX")"
if run_init_action "$INIT" "$ACTION" >"$SERVICE_OUT" 2>&1; then
rm -f "$SERVICE_OUT"
syslog_service_event "service $ACTION succeeded"
printf '{"ok":true,"action":'
json_string "$ACTION"
printf '}\n'
else
MSG="$(read_error_detail_file "$SERVICE_OUT" 2>/dev/null || true)"
rm -f "$SERVICE_OUT"
SHORT_MSG="$(printf '%.200s' "${MSG:-service action failed}")"
syslog_service_event "service $ACTION failed: $SHORT_MSG"
json_error "service_failed" "${MSG:-service action failed}"
return 1
fi
}
stat_mtime() {
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null || printf ''
}
emit_config_read() {
load_settings
if [ ! -r "$CONFIG_PATH" ]; then
printf '{"ok":false,"code":"config_not_found","message":"configuration file not found","path":'
json_string "$CONFIG_PATH"
printf '}\n'
return 1
fi
printf '{'
printf '"ok":true,'
printf '"path":'
json_string "$CONFIG_PATH"
printf ',"mtime":'
json_string "$(stat_mtime "$CONFIG_PATH")"
printf ',"content":'
json_file_string "$CONFIG_PATH"
printf '}\n'
}
safe_single_line_value() {
printf '%s' "$1" | awk '
BEGIN { ok = 1; cr = sprintf("%c", 13) }
NR > 1 { ok = 0 }
index($0, cr) { ok = 0 }
END { exit ok ? 0 : 1 }
'
}
json_field_string() {
NAME="$1"
VALUE="$2"
printf '"%s":' "$NAME"
json_string "$VALUE"
}
emit_settings_read() {
load_settings
TOKEN_SET=0
PROXY_SET=0
if [ -n "$GITHUB_TOKEN" ]; then TOKEN_SET=1; fi
if [ -n "$DOWNLOAD_PROXY" ]; then PROXY_SET=1; fi
printf '{'
printf '"ok":true,'
json_field_string core_repository "$CORE_REPOSITORY"
printf ','
json_field_string core_bundle "$CORE_BUNDLE"
printf ','
json_field_string config_path "$CONFIG_PATH"
printf ','
json_field_string working_dir "$WORKING_DIR"
printf ',"log_syslog":'
json_bool "$LOG_SYSLOG"
printf ','
json_field_string download_proxy "$DOWNLOAD_PROXY"
printf ','
json_field_string github_token ''
printf ',"download_proxy_set":'
json_bool "$PROXY_SET"
printf ',"github_token_set":'
json_bool "$TOKEN_SET"
printf '}\n'
}
settings_value() {
KEY="$1"
INPUT="$2"
VALUE="$(input_value "$KEY" "$INPUT")"
if ! safe_single_line_value "$VALUE"; then
return 1
fi
printf '%s' "$VALUE"
}
emit_settings_save() {
INPUT="$(cat)"
if ! command -v uci >/dev/null 2>&1; then
json_error "uci_unavailable" "uci command not found"
return 1
fi
load_settings
if ! REPOSITORY_VALUE="$(settings_value core_repository "$INPUT")" ||
! BUNDLE_VALUE="$(settings_value core_bundle "$INPUT")" ||
! CONFIG_VALUE="$(settings_value config_path "$INPUT")" ||
! WORKING_VALUE="$(settings_value working_dir "$INPUT")" ||
! PROXY_VALUE="$(settings_value download_proxy "$INPUT")"; then
json_error "invalid_settings" "settings values must be single-line strings"
return 1
fi
[ -n "$REPOSITORY_VALUE" ] || REPOSITORY_VALUE="$DEFAULT_CORE_REPOSITORY"
[ -n "$BUNDLE_VALUE" ] || BUNDLE_VALUE="$DEFAULT_CORE_BUNDLE"
[ -n "$CONFIG_VALUE" ] || CONFIG_VALUE="$DEFAULT_CONFIG_PATH"
[ -n "$WORKING_VALUE" ] || WORKING_VALUE="$DEFAULT_WORKING_DIR"
LOG_SYSLOG_VALUE="$LOG_SYSLOG"
if input_has_key log_syslog "$INPUT"; then
LOG_SYSLOG_VALUE="$(input_value log_syslog "$INPUT")"
case "$LOG_SYSLOG_VALUE" in
0|1) ;;
*)
json_error "invalid_log_syslog" "log_syslog must be 0 or 1"
return 1
;;
esac
fi
case "$REPOSITORY_VALUE" in
*/*) ;;
*)
json_error "invalid_core_repository" "core_repository must use owner/repo format"
return 1
;;
esac
if printf '%s' "$REPOSITORY_VALUE" | grep -q '[^A-Za-z0-9_.\/-]'; then
json_error "invalid_core_repository" "core_repository contains unsupported characters"
return 1
fi
case "$BUNDLE_VALUE" in
full|standard|minimal) ;;
*)
json_error "invalid_core_bundle" "core_bundle must be one of full, standard, minimal"
return 1
;;
esac
if ! valid_config_path_setting "$CONFIG_VALUE"; then
json_error "invalid_config_path" "config_path must be an absolute OxiDNS-related file path"
return 1
fi
if ! valid_working_dir_setting "$WORKING_VALUE"; then
json_error "invalid_working_dir" "working_dir must be an absolute OxiDNS-related directory path"
return 1
fi
uci -q set oxidns.main=settings
uci -q set "oxidns.main.core_repository=$REPOSITORY_VALUE"
uci -q set "oxidns.main.core_bundle=$BUNDLE_VALUE"
uci -q set "oxidns.main.config_path=$CONFIG_VALUE"
uci -q set "oxidns.main.working_dir=$WORKING_VALUE"
uci -q set "oxidns.main.log_syslog=$LOG_SYSLOG_VALUE"
uci -q delete oxidns.main.api_base_url >/dev/null 2>&1 || true
uci -q delete oxidns.main.install_mode >/dev/null 2>&1 || true
uci -q delete oxidns.main.service_name >/dev/null 2>&1 || true
uci -q delete oxidns.main.package_feed_url >/dev/null 2>&1 || true
uci -q delete oxidns.main.manifest_url >/dev/null 2>&1 || true
CLEAR_PROXY="$(input_bool clear_download_proxy "$INPUT")"
if [ "$CLEAR_PROXY" = "1" ]; then
uci -q delete oxidns.main.download_proxy >/dev/null 2>&1 || true
elif [ -n "$PROXY_VALUE" ]; then
uci -q set "oxidns.main.download_proxy=$PROXY_VALUE"
elif [ -z "$DOWNLOAD_PROXY" ]; then
uci -q delete oxidns.main.download_proxy >/dev/null 2>&1 || true
fi
NEW_TOKEN="$(input_value github_token "$INPUT")"
CLEAR_TOKEN="$(input_bool clear_github_token "$INPUT")"
if [ "$CLEAR_TOKEN" = "1" ]; then
uci -q delete oxidns.main.github_token >/dev/null 2>&1 || true
elif [ -n "$NEW_TOKEN" ]; then
if ! safe_single_line_value "$NEW_TOKEN"; then
json_error "invalid_token" "github_token must be a single-line value"
return 1
fi
uci -q set oxidns.main.github_token="$NEW_TOKEN"
fi
if uci -q commit oxidns; then
emit_settings_read
else
json_error "settings_save_failed" "failed to commit oxidns settings"
return 1
fi
}
input_value() {
KEY="$1"
INPUT="$2"
if command -v jsonfilter >/dev/null 2>&1; then
printf '%s' "$INPUT" | jsonfilter -e "@.$KEY" 2>/dev/null || true
else
printf '%s' "$INPUT" | sed -n "s/.*\"$KEY\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p"
fi
}
input_has_key() {
KEY="$1"
INPUT="$2"
printf '%s' "$INPUT" | grep -q "\"$KEY\"[[:space:]]*:"
}
input_bool() {
KEY="$1"
INPUT="$2"
VALUE="$(input_value "$KEY" "$INPUT")"
case "$VALUE" in
1|true|TRUE|yes|YES|on|ON)
printf '1'
;;
*)
printf '0'
;;
esac
}
path_is_under() {
CHILD="$1"
PARENT="$2"
[ -n "$CHILD" ] && [ -n "$PARENT" ] || return 1
case "$CHILD" in
"$PARENT"/*)
return 0
;;
esac
return 1
}
oxidns_named_path() {
PATH_VALUE="$1"
BASE_NAME="$(basename "$PATH_VALUE" 2>/dev/null || printf '')"
DIR_NAME="$(dirname "$PATH_VALUE" 2>/dev/null || printf '')"
DIR_BASE="$(basename "$DIR_NAME" 2>/dev/null || printf '')"
case "$BASE_NAME:$DIR_BASE" in
*oxidns*|*:oxidns*)
return 0
;;
esac
return 1
}
safe_remove_config_path() {
PATH_VALUE="$1"
[ -n "$PATH_VALUE" ] || return 1
case "$PATH_VALUE" in
/*) ;;
*) return 1 ;;
esac
[ -d "$PATH_VALUE" ] && return 1
oxidns_named_path "$PATH_VALUE"
}
safe_remove_working_dir() {
PATH_VALUE="$1"
[ -n "$PATH_VALUE" ] || return 1
case "$PATH_VALUE" in
/*) ;;
*) return 1 ;;
esac
case "$PATH_VALUE" in
/|/bin|/dev|/etc|/lib|/mnt|/overlay|/proc|/rom|/root|/sbin|/sys|/tmp|/usr|/var|/www)
return 1
;;
esac
oxidns_named_path "$PATH_VALUE"
}
path_has_parent_ref() {
case "$1" in
..|../*|*/..|*/../*)
return 0
;;
esac
return 1
}
valid_config_path_setting() {
PATH_VALUE="$1"
path_has_parent_ref "$PATH_VALUE" && return 1
safe_remove_config_path "$PATH_VALUE"
}
valid_working_dir_setting() {
PATH_VALUE="$1"
path_has_parent_ref "$PATH_VALUE" && return 1
safe_remove_working_dir "$PATH_VALUE"
}
validate_config_file() {
FILE="$1"
load_settings
if [ ! -x "$OXIDNS_BIN" ]; then
printf 'oxidns binary not found or not executable: %s' "$OXIDNS_BIN"
return 1
fi
mkdir -p "$WORKING_DIR"
"$OXIDNS_BIN" check -c "$FILE" -d "$WORKING_DIR" 2>&1
}
emit_config_validate() {
INPUT="$(cat)"
CONTENT="$(input_value content "$INPUT")"
TMP_FILE="$(mktemp "$TMP_ROOT/oxidns-config.XXXXXX")"
if input_has_key content "$INPUT"; then
if [ -z "$CONTENT" ]; then
rm -f "$TMP_FILE"
json_error "missing_content" "configuration content is required"
return 1
fi
printf '%s' "$CONTENT" > "$TMP_FILE"
elif [ -r "$CONFIG_PATH" ]; then
cp "$CONFIG_PATH" "$TMP_FILE"
else
rm -f "$TMP_FILE"
json_error "config_missing" "no configuration content or file available"
return 1
fi
if OUTPUT="$(validate_config_file "$TMP_FILE")"; then
rm -f "$TMP_FILE"
printf '{"ok":true,"message":'
json_string "${OUTPUT:-configuration is valid}"
printf '}\n'
else
rm -f "$TMP_FILE"
json_error "config_invalid" "$OUTPUT"
return 1
fi
}
emit_config_save() {
load_settings
INPUT="$(cat)"
CONTENT="$(input_value content "$INPUT")"
BASE_MTIME="$(input_value base_mtime "$INPUT")"
RESTART="$(input_value restart "$INPUT")"
[ -n "$RESTART" ] || RESTART="$(input_value reload "$INPUT")"
if [ -z "$CONTENT" ]; then
json_error "missing_content" "configuration content is required"
return 1
fi
if [ -n "$BASE_MTIME" ] && [ -e "$CONFIG_PATH" ] && [ "$BASE_MTIME" != "$(stat_mtime "$CONFIG_PATH")" ]; then
json_error "config_conflict" "configuration file changed since it was loaded"
return 1
fi
TMP_FILE="$(mktemp "$TMP_ROOT/oxidns-config.XXXXXX")"
printf '%s' "$CONTENT" > "$TMP_FILE"
if ! OUTPUT="$(validate_config_file "$TMP_FILE")"; then
rm -f "$TMP_FILE"
json_error "config_invalid" "$OUTPUT"
return 1
fi
mkdir -p "$(dirname "$CONFIG_PATH")"
if [ -e "$CONFIG_PATH" ]; then
mkdir -p "$WORKING_DIR/config-backups"
cp "$CONFIG_PATH" "$WORKING_DIR/config-backups/config.yaml.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
fi
mv "$TMP_FILE" "$CONFIG_PATH"
RESTART_STATUS="not_requested"
MESSAGE="configuration saved"
if [ "$RESTART" = "true" ] || [ "$RESTART" = "1" ]; then
INIT="$(init_script)"
SERVICE_OUT="$(mktemp "$TMP_ROOT/oxidns-service.XXXXXX")"
if [ ! -x "$INIT" ]; then
rm -f "$SERVICE_OUT"
json_error "service_unavailable" "configuration saved, but init script was not found"
return 1
fi
if run_init_action "$INIT" restart >"$SERVICE_OUT" 2>&1; then
RESTART_STATUS="requested"
MESSAGE="configuration saved and service restarted"
else
MSG="$(read_error_detail_file "$SERVICE_OUT" 2>/dev/null || true)"
rm -f "$SERVICE_OUT"
json_error "service_restart_failed" "configuration saved, but service restart failed: ${MSG:-service restart failed}"
return 1
fi
rm -f "$SERVICE_OUT"
fi
printf '{"ok":true,"path":'
json_string "$CONFIG_PATH"
printf ',"mtime":'
json_string "$(stat_mtime "$CONFIG_PATH")"
printf ',"restart":'
json_string "$RESTART_STATUS"
printf ',"message":'
json_string "$MESSAGE"
printf '}\n'
}
emit_log_lines_json() {
SOURCE="$1"
FILE="$2"
printf '{"ok":true,"source":'
json_string "$SOURCE"
printf ',"lines":['
SEP=""
while IFS= read -r LINE; do
printf '%s' "$SEP"
json_string "$LINE"
SEP=","
done < "$FILE"
printf ']}\n'
}
read_logread_logs() {
LIMIT="$1"
OUT_FILE="$2"
SCAN_LIMIT="$((LIMIT * 4))"
: > "$OUT_FILE"
if ! command -v logread >/dev/null 2>&1; then
return 1
fi
if [ "$SCAN_LIMIT" -lt 100 ]; then
SCAN_LIMIT=100
elif [ "$SCAN_LIMIT" -gt 2000 ]; then
SCAN_LIMIT=2000
fi
logread -l "$SCAN_LIMIT" -e oxidns 2>/dev/null | awk '
BEGIN {
esc = sprintf("%c", 27);
csi = esc "\\[[0-9;?]*[ -/]*[@-~]";
}
/oxidns\[[0-9]+\]:/ {
gsub(csi, "");
gsub(/\r/, "");
sub(/^.* oxidns\[[0-9]+\]:[[:space:]]*/, "");
print;
}
' | tail -n "$LIMIT" > "$OUT_FILE" || true
}
emit_logs_recent() {
INPUT="$(cat)"
LIMIT="$(input_value limit "$INPUT")"
[ -n "$LIMIT" ] || LIMIT=200
case "$LIMIT" in
*[!0-9]*|'') LIMIT=200 ;;
esac
TMP_FILE="$(mktemp "$TMP_ROOT/oxidns-logs.XXXXXX")"
read_logread_logs "$LIMIT" "$TMP_FILE" || true
emit_log_lines_json "logread" "$TMP_FILE"
rm -f "$TMP_FILE"
}
read_error_detail_file() {
DETAIL_FILE="$1"
[ -r "$DETAIL_FILE" ] || return 0
awk -v token="$GITHUB_TOKEN" -v proxy="$DOWNLOAD_PROXY" '
function redact(value, secret, out, pos) {
if (secret == "")
return value;
out = "";
while ((pos = index(value, secret)) > 0) {
out = out substr(value, 1, pos - 1) "[redacted]";
value = substr(value, pos + length(secret));
}
return out value;
}
{
line = redact(redact($0, token), proxy);
gsub(/[[:cntrl:]]/, " ", line);
print line;
}
' "$DETAIL_FILE" | tail -c 1200
}
set_download_error_from_file() {
ERROR_FILE="$1"
ERROR_RC="$2"
ERROR_TOOL="$3"
ERROR_DETAIL="$(read_error_detail_file "$ERROR_FILE" 2>/dev/null || true)"
ERROR_DETAIL="$(printf '%s' "$ERROR_DETAIL" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
[ -n "$ERROR_DETAIL" ] || ERROR_DETAIL="$ERROR_TOOL exited with code $ERROR_RC"
DOWNLOAD_RC="$ERROR_RC"
DOWNLOAD_ERROR="$ERROR_TOOL exited with code $ERROR_RC: $ERROR_DETAIL"
rm -f "$ERROR_FILE"
}
run_download_command() {
DOWNLOAD_TOOL="$1"
DOWNLOAD_DISPLAY="$2"
DOWNLOAD_OUT="$3"
shift 3
DOWNLOAD_ERR_FILE="$(mktemp "$TMP_ROOT/oxidns-download.XXXXXX")"
DOWNLOAD_DONE_FILE="$(mktemp "$TMP_ROOT/oxidns-download-done.XXXXXX")"
rm -f "$DOWNLOAD_DONE_FILE"
core_progress_command "$DOWNLOAD_DISPLAY"
case "$-" in
*e*) DOWNLOAD_RESTORE_ERREXIT=1 ;;
*) DOWNLOAD_RESTORE_ERREXIT=0 ;;
esac
set +e
DOWNLOAD_LAST_SIZE=""
(
while [ ! -f "$DOWNLOAD_DONE_FILE" ]; do
if [ -f "$DOWNLOAD_OUT" ]; then
DOWNLOAD_SIZE="$(file_size_bytes "$DOWNLOAD_OUT")"
if [ "$DOWNLOAD_SIZE" != "$DOWNLOAD_LAST_SIZE" ]; then
core_progress_log "progress" "$DOWNLOAD_TOOL downloaded $DOWNLOAD_SIZE bytes"
DOWNLOAD_LAST_SIZE="$DOWNLOAD_SIZE"
fi
fi
sleep 1
done
) &
DOWNLOAD_MONITOR_PID="$!"
"$@" >/dev/null 2>"$DOWNLOAD_ERR_FILE"
DOWNLOAD_CMD_RC="$?"
: > "$DOWNLOAD_DONE_FILE"
wait "$DOWNLOAD_MONITOR_PID" 2>/dev/null || true
rm -f "$DOWNLOAD_DONE_FILE"
if [ "$DOWNLOAD_RESTORE_ERREXIT" = "1" ]; then
set -e
else
set +e
fi
if [ "$DOWNLOAD_CMD_RC" -eq 0 ]; then
rm -f "$DOWNLOAD_ERR_FILE"
DOWNLOAD_ERROR=""
DOWNLOAD_RC=0
core_progress_log "success" "$DOWNLOAD_TOOL download complete: $(file_size_bytes "$DOWNLOAD_OUT") bytes"
return 0
fi
set_download_error_from_file "$DOWNLOAD_ERR_FILE" "$DOWNLOAD_CMD_RC" "$DOWNLOAD_TOOL"
core_progress_log "error" "$DOWNLOAD_ERROR"
return "$DOWNLOAD_CMD_RC"
}
download_file() {
URL="$1"
OUT="$2"
DOWNLOAD_ERROR=""
DOWNLOAD_RC=""
if command -v curl >/dev/null 2>&1; then
if [ -n "$DOWNLOAD_PROXY" ]; then
if [ -n "$GITHUB_TOKEN" ]; then
run_download_command curl "curl -fsSL --proxy [configured] -H 'User-Agent: luci-app-oxidns' -H 'Authorization: Bearer [redacted]' -o $OUT $URL" "$OUT" curl -fsSL --proxy "$DOWNLOAD_PROXY" -H "User-Agent: luci-app-oxidns" -H "Authorization: Bearer $GITHUB_TOKEN" -o "$OUT" "$URL"
else
run_download_command curl "curl -fsSL --proxy [configured] -H 'User-Agent: luci-app-oxidns' -o $OUT $URL" "$OUT" curl -fsSL --proxy "$DOWNLOAD_PROXY" -H "User-Agent: luci-app-oxidns" -o "$OUT" "$URL"
fi
elif [ -n "$GITHUB_TOKEN" ]; then
run_download_command curl "curl -fsSL -H 'User-Agent: luci-app-oxidns' -H 'Authorization: Bearer [redacted]' -o $OUT $URL" "$OUT" curl -fsSL -H "User-Agent: luci-app-oxidns" -H "Authorization: Bearer $GITHUB_TOKEN" -o "$OUT" "$URL"
else
run_download_command curl "curl -fsSL -H 'User-Agent: luci-app-oxidns' -o $OUT $URL" "$OUT" curl -fsSL -H "User-Agent: luci-app-oxidns" -o "$OUT" "$URL"
fi
elif command -v uclient-fetch >/dev/null 2>&1; then
if [ -n "$GITHUB_TOKEN" ]; then
run_download_command uclient-fetch "uclient-fetch -O $OUT --header='User-Agent: luci-app-oxidns' --header='Authorization: Bearer [redacted]' $URL" "$OUT" uclient-fetch -O "$OUT" --header="User-Agent: luci-app-oxidns" --header="Authorization: Bearer $GITHUB_TOKEN" "$URL"
else
run_download_command uclient-fetch "uclient-fetch -O $OUT --header='User-Agent: luci-app-oxidns' $URL" "$OUT" uclient-fetch -O "$OUT" --header="User-Agent: luci-app-oxidns" "$URL"
fi
elif command -v wget >/dev/null 2>&1; then
if [ -n "$GITHUB_TOKEN" ]; then
run_download_command wget "wget --header='User-Agent: luci-app-oxidns' --header='Authorization: Bearer [redacted]' -O $OUT $URL" "$OUT" wget --header="User-Agent: luci-app-oxidns" --header="Authorization: Bearer $GITHUB_TOKEN" -O "$OUT" "$URL"
else
run_download_command wget "wget --header='User-Agent: luci-app-oxidns' -O $OUT $URL" "$OUT" wget --header="User-Agent: luci-app-oxidns" -O "$OUT" "$URL"
fi
else
DOWNLOAD_RC=127
DOWNLOAD_ERROR="none of curl, uclient-fetch, or wget is installed"
core_progress_log "error" "$DOWNLOAD_ERROR"
return 1
fi
}
require_download_transport() {
if [ -n "$DOWNLOAD_PROXY" ] && ! command -v curl >/dev/null 2>&1; then
core_progress_log "error" "download_proxy requires curl, but curl is not installed"
json_detailed_error "download_proxy_requires_curl" "download_proxy is configured, but curl is not installed; install curl or clear download_proxy" "prepare_download" "download_proxy only works with curl on OpenWrt"
return 1
fi
return 0
}
sha256_file() {
sha256sum "$1" 2>/dev/null | awk '{ print $1 }'
}
asset_name_for_bundle() {
BUNDLE="$1"
RUST_TARGET="$2"
case "$BUNDLE" in
full)
printf 'oxidns-%s.tar.gz' "$RUST_TARGET"
;;
standard|minimal)
printf 'oxidns-%s-%s.tar.gz' "$BUNDLE" "$RUST_TARGET"
;;
*)
return 1
;;
esac
}
release_api_url() {
REPOSITORY="$1"
TARGET="$2"
if [ "$TARGET" = "latest" ]; then
printf '%s/repos/%s/releases/latest' "$GITHUB_API_BASE" "$REPOSITORY"
else
printf '%s/repos/%s/releases/tags/%s' "$GITHUB_API_BASE" "$REPOSITORY" "$TARGET"
fi
}
version_tag() {
VERSION="$1"
case "$VERSION" in
v*) printf '%s' "$VERSION" ;;
*) printf 'v%s' "$VERSION" ;;
esac
}
select_release_asset() {
RELEASE_FILE="$1"
WANT_ASSET="$2"
SELECT_TAG=""
SELECT_ASSET_NAME=""
SELECT_URL=""
SELECT_SHA256=""
SELECT_SIZE=""
if ! command -v jsonfilter >/dev/null 2>&1; then
return 2
fi
SELECT_TAG="$(jsonfilter -i "$RELEASE_FILE" -e '@.tag_name' 2>/dev/null || true)"
[ -n "$SELECT_TAG" ] || return 1
NAME_FILE="$(mktemp "$TMP_ROOT/oxidns-asset-name.XXXXXX")"
URL_FILE="$(mktemp "$TMP_ROOT/oxidns-asset-url.XXXXXX")"
DIGEST_FILE="$(mktemp "$TMP_ROOT/oxidns-asset-digest.XXXXXX")"
SIZE_FILE="$(mktemp "$TMP_ROOT/oxidns-asset-size.XXXXXX")"
SELECT_FILE="$(mktemp "$TMP_ROOT/oxidns-asset-selected.XXXXXX")"
if ! jsonfilter -i "$RELEASE_FILE" -e '@.assets[*].name' > "$NAME_FILE" 2>/dev/null ||
! jsonfilter -i "$RELEASE_FILE" -e '@.assets[*].browser_download_url' > "$URL_FILE" 2>/dev/null ||
! jsonfilter -i "$RELEASE_FILE" -e '@.assets[*].digest' > "$DIGEST_FILE" 2>/dev/null ||
! jsonfilter -i "$RELEASE_FILE" -e '@.assets[*].size' > "$SIZE_FILE" 2>/dev/null; then
rm -f "$NAME_FILE" "$URL_FILE" "$DIGEST_FILE" "$SIZE_FILE" "$SELECT_FILE"
return 1
fi
if ! awk -v want="$WANT_ASSET" -v url_file="$URL_FILE" -v digest_file="$DIGEST_FILE" -v size_file="$SIZE_FILE" '
BEGIN {
while ((getline url < url_file) > 0)
urls[++url_count] = url;
while ((getline digest < digest_file) > 0)
digests[++digest_count] = digest;
while ((getline size < size_file) > 0)
sizes[++size_count] = size;
}
{
idx++;
if ($0 == want) {
print $0;
print urls[idx];
print digests[idx];
print sizes[idx];
found = 1;
exit;
}
}
END {
exit found ? 0 : 1;
}
' "$NAME_FILE" > "$SELECT_FILE"; then
rm -f "$NAME_FILE" "$URL_FILE" "$DIGEST_FILE" "$SIZE_FILE" "$SELECT_FILE"
return 1
fi
SELECT_ASSET_NAME="$(sed -n '1p' "$SELECT_FILE")"
SELECT_URL="$(sed -n '2p' "$SELECT_FILE")"
SELECT_SHA256="$(sed -n '3p' "$SELECT_FILE" | sed 's/^sha256://')"
SELECT_SIZE="$(sed -n '4p' "$SELECT_FILE")"
rm -f "$NAME_FILE" "$URL_FILE" "$DIGEST_FILE" "$SIZE_FILE" "$SELECT_FILE"
[ -n "$SELECT_ASSET_NAME" ] && [ -n "$SELECT_URL" ]
}
valid_sha256() {
printf '%s' "$1" | grep -Eq '^[0-9a-fA-F]{64}$'
}
validate_safe_archive() {
ARCHIVE="$1"
if ! tar -tzf "$ARCHIVE" | awk '
{
path = $0;
while (path ~ /^\.\//)
sub(/^\.\//, "", path);
while (path ~ /\/$/)
sub(/\/$/, "", path);
if (path == "" || path ~ /^\//)
bad = 1;
n = split(path, parts, "/");
for (i = 1; i <= n; i++) {
if (parts[i] == "" || parts[i] == "..")
bad = 1;
}
}
END { exit bad ? 1 : 0 }
'; then
return 1
fi
if ! tar -tvzf "$ARCHIVE" | awk '
{
type = substr($0, 1, 1);
if (type != "-" && type != "d")
bad = 1;
}
END { exit bad ? 1 : 0 }
'; then
return 1
fi
return 0
}
valid_ipv4() {
printf '%s' "$1" | awk -F. '
NF != 4 { exit 1 }
{
for (i = 1; i <= 4; i++) {
if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255)
exit 1;
}
}
'
}
lan_ipv4_from_uci() {
command -v uci >/dev/null 2>&1 || return 1
LAN_IP="$(uci -q get network.lan.ipaddr 2>/dev/null || true)"
if valid_ipv4 "$LAN_IP"; then
printf '%s' "$LAN_IP"
return 0
fi
return 1
}
lan_ipv4_from_interface() {
IFACE="${1:-br-lan}"
command -v ip >/dev/null 2>&1 || return 1
ip -4 addr show dev "$IFACE" 2>/dev/null | awk '
/ inet / {
addr = $2;
sub(/\/.*/, "", addr);
print addr;
exit;
}
'
}
default_api_listen() {
LAN_IP="$(lan_ipv4_from_uci 2>/dev/null || true)"
if [ -z "$LAN_IP" ]; then
LAN_IP="$(lan_ipv4_from_interface br-lan 2>/dev/null || true)"
fi
if ! valid_ipv4 "$LAN_IP"; then
LAN_IP="0.0.0.0"
fi
printf '%s:9199' "$LAN_IP"
}
write_openwrt_default_config() {
SOURCE_FILE="$1"
OUT_FILE="$2"
DEFAULT_LISTEN="$(default_api_listen)"
awk -v default_listen="$DEFAULT_LISTEN" '
BEGIN {
top = "";
second = "";
third = "";
listen_done = 0;
root_done = 0;
}
/^[^[:space:]#][^:]*:/ {
top = $0;
sub(/:.*/, "", top);
second = "";
third = "";
}
top == "api" && /^[[:space:]][[:space:]]http:[[:space:]]*$/ {
second = "http";
third = "";
print;
next;
}
top == "api" && second == "http" && /^[[:space:]][[:space:]][[:space:]][[:space:]]webui:[[:space:]]*$/ {
third = "webui";
print;
next;
}
top == "api" && second == "http" && /^[[:space:]][[:space:]][[:space:]][[:space:]]listen:[[:space:]]*/ {
print " listen: \"" default_listen "\"";
listen_done = 1;
next;
}
top == "api" && second == "http" && third == "webui" && /^[[:space:]][[:space:]][[:space:]][[:space:]][[:space:]][[:space:]]root:[[:space:]]*/ {
print " root: \"/usr/share/oxidns/webui\"";
root_done = 1;
next;
}
{ print }
END {
if (!listen_done || !root_done)
exit 42;
}
' "$SOURCE_FILE" > "$OUT_FILE"
}
core_install_error() {
ERROR_CODE="$1"
ERROR_MESSAGE="$2"
ERROR_STAGE="${3:-}"
ERROR_DETAIL="${4:-}"
rm -rf "${CORE_TMP_DIR:-}" 2>/dev/null || true
core_progress_log "error" "$ERROR_CODE: $ERROR_MESSAGE"
[ -n "$ERROR_STAGE" ] && core_progress_log "error" "stage=$ERROR_STAGE"
[ -n "$ERROR_DETAIL" ] && core_progress_log "error" "$ERROR_DETAIL"
json_detailed_error "$ERROR_CODE" "$ERROR_MESSAGE" "$ERROR_STAGE" "$ERROR_DETAIL"
return 1
}
install_core_from_release() {
ACTION="$1"
RELEASE_TARGET="$2"
REQUEST_BUNDLE="$3"
load_settings
require_download_transport || return 1
case "$CORE_REPOSITORY" in
*/*) ;;
*)
core_progress_log "error" "invalid core_repository: $CORE_REPOSITORY"
json_error "invalid_core_repository" "core_repository must use owner/repo format"
return 1
;;
esac
core_progress_command "uname -m"
if ! RUST_TARGET="$(detect_rust_target 2>/dev/null)"; then
core_progress_log "error" "unsupported CPU architecture"
json_error "unsupported_target" "current CPU architecture does not match a published OxiDNS Linux musl release"
return 1
fi
core_progress_log "info" "target=$RUST_TARGET"
if ! ASSET_NAME="$(asset_name_for_bundle "$REQUEST_BUNDLE" "$RUST_TARGET")"; then
core_progress_log "error" "unsupported bundle: $REQUEST_BUNDLE"
json_error "unsupported_core_bundle" "core bundle must be one of full, standard, minimal"
return 1
fi
core_progress_log "info" "repository=$CORE_REPOSITORY bundle=$REQUEST_BUNDLE asset=$ASSET_NAME"
CORE_TMP_DIR="$(mktemp -d "$TMP_ROOT/oxidns-core.XXXXXX")"
RELEASE_FILE="$CORE_TMP_DIR/release.json"
ARCHIVE_FILE="$CORE_TMP_DIR/$ASSET_NAME"
UNPACK_DIR="$CORE_TMP_DIR/unpack"
RELEASE_URL="$(release_api_url "$CORE_REPOSITORY" "$RELEASE_TARGET")"
core_progress_log "info" "fetch release metadata: $RELEASE_URL"
if ! download_file "$RELEASE_URL" "$RELEASE_FILE"; then
core_install_error "release_fetch_failed" "failed to fetch OxiDNS release metadata" "download_metadata" "repository=$CORE_REPOSITORY release=$RELEASE_TARGET url=$RELEASE_URL; ${DOWNLOAD_ERROR:-unknown download error}"
return 1
fi
core_progress_command "jsonfilter release metadata for $ASSET_NAME"
set +e
select_release_asset "$RELEASE_FILE" "$ASSET_NAME"
SELECT_RELEASE_RC="$?"
set -e
if [ "$SELECT_RELEASE_RC" -ne 0 ]; then
if [ "$SELECT_RELEASE_RC" -eq 2 ]; then
SELECT_RELEASE_DETAIL="jsonfilter is not installed or unavailable; wanted asset=$ASSET_NAME repository=$CORE_REPOSITORY release=$RELEASE_TARGET"
else
SELECT_RELEASE_DETAIL="wanted asset=$ASSET_NAME repository=$CORE_REPOSITORY release=$RELEASE_TARGET"
fi
core_install_error "release_asset_not_found" "selected OxiDNS release does not contain the required archive" "select_release_asset" "$SELECT_RELEASE_DETAIL"
return 1
fi
core_progress_log "success" "selected release=$SELECT_TAG asset=$SELECT_ASSET_NAME size=${SELECT_SIZE:-unknown}"
if ! valid_sha256 "$SELECT_SHA256"; then
core_install_error "release_digest_missing" "selected OxiDNS release asset does not include a valid SHA256 digest" "verify_release_digest" "asset=$SELECT_ASSET_NAME digest=${SELECT_SHA256:-missing}"
return 1
fi
core_progress_log "info" "download archive: $SELECT_URL"
if ! download_file "$SELECT_URL" "$ARCHIVE_FILE"; then
core_install_error "core_download_failed" "failed to download OxiDNS core archive" "download_archive" "asset=$SELECT_ASSET_NAME url=$SELECT_URL; ${DOWNLOAD_ERROR:-unknown download error}"
return 1
fi
core_progress_command "sha256sum $ARCHIVE_FILE"
ACTUAL_SHA256="$(sha256_file "$ARCHIVE_FILE" || true)"
if [ -z "$ACTUAL_SHA256" ] || [ "$ACTUAL_SHA256" != "$SELECT_SHA256" ]; then
core_install_error "core_checksum_failed" "OxiDNS core archive SHA256 does not match the release digest" "verify_checksum" "asset=$SELECT_ASSET_NAME expected=$SELECT_SHA256 actual=${ACTUAL_SHA256:-missing}"
return 1
fi
core_progress_log "success" "checksum verified: $ACTUAL_SHA256"
core_progress_command "tar -tzf $ARCHIVE_FILE"
if ! validate_safe_archive "$ARCHIVE_FILE"; then
core_install_error "core_archive_unsafe" "OxiDNS core archive contains unsafe paths or links" "validate_archive" "asset=$SELECT_ASSET_NAME"
return 1
fi
mkdir -p "$UNPACK_DIR"
TAR_OUT="$(mktemp "$TMP_ROOT/oxidns-tar.XXXXXX")"
core_progress_command "tar -xzf $ARCHIVE_FILE -C $UNPACK_DIR"
if ! tar -xzf "$ARCHIVE_FILE" -C "$UNPACK_DIR" >"$TAR_OUT" 2>&1; then
TAR_DETAIL="$(read_error_detail_file "$TAR_OUT" 2>/dev/null || true)"
rm -f "$TAR_OUT"
core_install_error "core_unpack_failed" "failed to unpack OxiDNS core archive" "unpack_archive" "${TAR_DETAIL:-tar extraction failed}"
return 1
fi
rm -f "$TAR_OUT"
if [ ! -f "$UNPACK_DIR/oxidns" ]; then
core_install_error "core_archive_invalid" "OxiDNS core archive does not contain an oxidns binary" "install_files" "asset=$SELECT_ASSET_NAME"
return 1
fi
core_progress_log "success" "archive unpacked"
CONFIG_TMP=""
if [ ! -e "$CONFIG_PATH" ]; then
if [ ! -f "$UNPACK_DIR/config.yaml" ]; then
core_install_error "core_archive_invalid" "OxiDNS core archive does not contain config.yaml" "install_files" "asset=$SELECT_ASSET_NAME"
return 1
fi
CONFIG_TMP="$CORE_TMP_DIR/config.yaml.openwrt"
core_progress_command "adapt config.yaml for OpenWrt defaults"
if ! write_openwrt_default_config "$UNPACK_DIR/config.yaml" "$CONFIG_TMP"; then
core_install_error "default_config_unsupported" "release config.yaml cannot be adapted for OpenWrt defaults" "install_files" "config=$UNPACK_DIR/config.yaml"
return 1
fi
fi
SERVICE_RESTART="not_requested"
SERVICE_WAS_RUNNING=0
if service_running; then SERVICE_WAS_RUNNING=1; fi
INIT="$(init_script)"
if [ "$SERVICE_WAS_RUNNING" = "1" ] && [ -x "$INIT" ]; then
core_progress_command "$INIT stop"
SERVICE_OUT="$(mktemp "$TMP_ROOT/oxidns-service.XXXXXX")"
if ! run_init_action "$INIT" stop >"$SERVICE_OUT" 2>&1; then
MSG="$(read_error_detail_file "$SERVICE_OUT" 2>/dev/null || true)"
rm -f "$SERVICE_OUT"
core_install_error "service_stop_failed" "core was not replaced because the service could not be stopped" "stop_service" "${MSG:-service stop failed}"
return 1
fi
rm -f "$SERVICE_OUT"
fi
core_progress_command "install -m 755 $UNPACK_DIR/oxidns $OXIDNS_BIN"
mkdir -p "$(dirname "$OXIDNS_BIN")" "$WORKING_DIR"
cp "$UNPACK_DIR/oxidns" "$CORE_TMP_DIR/oxidns.new"
chmod 755 "$CORE_TMP_DIR/oxidns.new"
mv "$CORE_TMP_DIR/oxidns.new" "$OXIDNS_BIN"
if [ -d "$UNPACK_DIR/webui" ]; then
core_progress_command "copy webui files to $WEBUI_DIR"
WEBUI_TMP="$CORE_TMP_DIR/webui.new"
rm -rf "$WEBUI_TMP"
mkdir -p "$(dirname "$WEBUI_DIR")" "$WEBUI_TMP"
cp -R "$UNPACK_DIR/webui/." "$WEBUI_TMP/"
rm -rf "$WEBUI_DIR"
mv "$WEBUI_TMP" "$WEBUI_DIR"
fi
if [ -n "$CONFIG_TMP" ]; then
core_progress_command "install default config to $CONFIG_PATH"
mkdir -p "$(dirname "$CONFIG_PATH")"
mv "$CONFIG_TMP" "$CONFIG_PATH"
elif [ -f "$UNPACK_DIR/config.yaml" ]; then
core_progress_command "copy config example to $WORKING_DIR/config.yaml.example"
cp "$UNPACK_DIR/config.yaml" "$WORKING_DIR/config.yaml.example" 2>/dev/null || true
fi
CONFIG_CHECK_OK=0
core_progress_command "$OXIDNS_BIN check -c $CONFIG_PATH -d $WORKING_DIR"
if CONFIG_CHECK_OUTPUT="$(validate_config_file "$CONFIG_PATH" 2>&1)"; then
CONFIG_CHECK_OK=1
core_progress_log "success" "$CONFIG_CHECK_OUTPUT"
else
CONFIG_CHECK_OUTPUT="${CONFIG_CHECK_OUTPUT:-configuration check failed}"
core_progress_log "error" "$CONFIG_CHECK_OUTPUT"
fi
if [ "$SERVICE_WAS_RUNNING" = "1" ] && [ -x "$INIT" ]; then
SERVICE_OUT="$(mktemp "$TMP_ROOT/oxidns-service.XXXXXX")"
core_progress_command "$INIT start"
if "$INIT" start >"$SERVICE_OUT" 2>&1; then
SERVICE_RESTART="requested"
core_progress_log "success" "service restarted"
else
MSG="$(read_error_detail_file "$SERVICE_OUT" 2>/dev/null || true)"
rm -f "$SERVICE_OUT"
core_install_error "service_restart_failed" "core installed, but service restart failed" "restart_service" "${MSG:-service restart failed}"
return 1
fi
rm -f "$SERVICE_OUT"
fi
rm -rf "$CORE_TMP_DIR"
core_progress_log "success" "core $ACTION completed: version=$SELECT_TAG bundle=$REQUEST_BUNDLE target=$RUST_TARGET"
printf '{"ok":true,"action":'
json_string "$ACTION"
printf ',"version":'
json_string "$SELECT_TAG"
printf ',"bundle":'
json_string "$REQUEST_BUNDLE"
printf ',"target":'
json_string "$RUST_TARGET"
printf ',"asset":'
json_string "$SELECT_ASSET_NAME"
printf ',"binary_path":'
json_string "$OXIDNS_BIN"
printf ',"webui_path":'
json_string "$WEBUI_DIR"
printf ',"service_restart":'
json_string "$SERVICE_RESTART"
printf ',"config_check":{"ok":'
json_bool "$CONFIG_CHECK_OK"
printf ',"message":'
json_string "$CONFIG_CHECK_OUTPUT"
printf '}}\n'
}
emit_core_install_locked() {
load_settings
core_progress_reset "install OxiDNS core"
if [ -x "$OXIDNS_BIN" ]; then
core_progress_log "error" "core already installed"
json_error "core_already_installed" "OxiDNS core is already installed; use repair reinstall instead"
return 1
fi
install_core_from_release install latest "$CORE_BUNDLE"
}
emit_core_install() {
run_with_core_lock emit_core_install_locked
}
emit_core_reinstall_locked() {
load_settings
core_progress_reset "repair reinstall OxiDNS core"
BUILD_JSON="$(build_info_json)"
CURRENT_VERSION="$(json_value_from_string version "$BUILD_JSON")"
CURRENT_BUNDLE="$(json_value_from_string bundle "$BUILD_JSON")"
if [ ! -x "$OXIDNS_BIN" ] || [ -z "$CURRENT_VERSION" ]; then
core_progress_log "error" "core is not installed or cannot report its version"
json_error "core_not_installed" "OxiDNS core is not installed or cannot report its version"
return 1
fi
core_progress_log "info" "current_version=$CURRENT_VERSION current_bundle=${CURRENT_BUNDLE:-$CORE_BUNDLE}"
[ -n "$CURRENT_BUNDLE" ] || CURRENT_BUNDLE="$CORE_BUNDLE"
install_core_from_release reinstall "$(version_tag "$CURRENT_VERSION")" "$CURRENT_BUNDLE"
}
emit_core_reinstall() {
run_with_core_lock emit_core_reinstall_locked
}
upload_core_error() {
ERROR_CODE="$1"
ERROR_MESSAGE="$2"
ERROR_STAGE="${3:-}"
ERROR_DETAIL="${4:-}"
rm -rf "${CORE_TMP_DIR:-}" 2>/dev/null || true
[ -n "${UPLOAD_PATH:-}" ] && rm -f "$UPLOAD_PATH" 2>/dev/null || true
core_progress_log "error" "$ERROR_CODE: $ERROR_MESSAGE"
[ -n "$ERROR_STAGE" ] && core_progress_log "error" "stage=$ERROR_STAGE"
[ -n "$ERROR_DETAIL" ] && core_progress_log "error" "$ERROR_DETAIL"
json_detailed_error "$ERROR_CODE" "$ERROR_MESSAGE" "$ERROR_STAGE" "$ERROR_DETAIL"
return 1
}
valid_upload_path() {
case "$1" in
/tmp/oxidns-core-upload-*) return 0 ;;
esac
return 1
}
validate_uploaded_binary() {
BIN="$1"
chmod 755 "$BIN"
if command -v hexdump >/dev/null 2>&1; then
MAGIC="$(dd if="$BIN" bs=4 count=1 2>/dev/null | hexdump -v -e '1/1 "%02x"')"
[ "$MAGIC" = "7f454c46" ] || return 1
elif command -v od >/dev/null 2>&1; then
MAGIC="$(dd if="$BIN" bs=4 count=1 2>/dev/null | od -An -tx1 | tr -d ' \n')"
[ "$MAGIC" = "7f454c46" ] || return 1
fi
CHECK_BIN="$(dirname "$OXIDNS_BIN")/.oxidns-upload-check.$$"
mkdir -p "$(dirname "$OXIDNS_BIN")"
cp "$BIN" "$CHECK_BIN" || return 1
chmod 755 "$CHECK_BIN"
set +e
"$CHECK_BIN" build-info >/dev/null 2>&1
VALIDATE_RC="$?"
set -e
rm -f "$CHECK_BIN"
[ "$VALIDATE_RC" -eq 0 ]
}
install_uploaded_core_from_unpack() {
UPLOAD_KIND="$1"
UNPACK_DIR="$2"
HAS_WEBUI=0
CONFIG_CHECK_OK=0
CONFIG_CHECK_OUTPUT=""
SERVICE_RESTART="not_requested"
if [ ! -f "$UNPACK_DIR/oxidns" ]; then
upload_core_error "uploaded_core_invalid" "uploaded archive does not contain an oxidns binary" "validate_uploaded_core" "expected file: oxidns"
return 1
fi
core_progress_command "validate uploaded oxidns binary"
if ! validate_uploaded_binary "$UNPACK_DIR/oxidns"; then
upload_core_error "uploaded_core_invalid" "uploaded oxidns binary cannot report build-info" "validate_uploaded_core" "the uploaded binary either is not executable for this target or build-info failed"
return 1
fi
core_progress_log "success" "uploaded binary passed validation"
CONFIG_TMP=""
if [ ! -e "$CONFIG_PATH" ] && [ -f "$UNPACK_DIR/config.yaml" ]; then
CONFIG_TMP="$CORE_TMP_DIR/config.yaml.openwrt"
core_progress_command "adapt uploaded config.yaml for OpenWrt defaults"
if ! write_openwrt_default_config "$UNPACK_DIR/config.yaml" "$CONFIG_TMP"; then
upload_core_error "default_config_unsupported" "uploaded config.yaml cannot be adapted for OpenWrt defaults" "install_files" "config=$UNPACK_DIR/config.yaml"
return 1
fi
fi
SERVICE_WAS_RUNNING=0
if service_running; then SERVICE_WAS_RUNNING=1; fi
INIT="$(init_script)"
if [ "$SERVICE_WAS_RUNNING" = "1" ] && [ -x "$INIT" ]; then
core_progress_command "$INIT stop"
SERVICE_OUT="$(mktemp "$TMP_ROOT/oxidns-service.XXXXXX")"
if ! run_init_action "$INIT" stop >"$SERVICE_OUT" 2>&1; then
MSG="$(read_error_detail_file "$SERVICE_OUT" 2>/dev/null || true)"
rm -f "$SERVICE_OUT"
upload_core_error "service_stop_failed" "core was not replaced because the service could not be stopped" "stop_service" "${MSG:-service stop failed}"
return 1
fi
rm -f "$SERVICE_OUT"
fi
core_progress_command "install -m 755 $UNPACK_DIR/oxidns $OXIDNS_BIN"
mkdir -p "$(dirname "$OXIDNS_BIN")" "$WORKING_DIR"
cp "$UNPACK_DIR/oxidns" "$CORE_TMP_DIR/oxidns.new"
chmod 755 "$CORE_TMP_DIR/oxidns.new"
mv "$CORE_TMP_DIR/oxidns.new" "$OXIDNS_BIN"
if [ -d "$UNPACK_DIR/webui" ]; then
core_progress_command "copy webui files to $WEBUI_DIR"
WEBUI_TMP="$CORE_TMP_DIR/webui.new"
rm -rf "$WEBUI_TMP"
mkdir -p "$(dirname "$WEBUI_DIR")" "$WEBUI_TMP"
cp -R "$UNPACK_DIR/webui/." "$WEBUI_TMP/"
rm -rf "$WEBUI_DIR"
mv "$WEBUI_TMP" "$WEBUI_DIR"
HAS_WEBUI=1
fi
if [ -n "$CONFIG_TMP" ]; then
core_progress_command "install default config to $CONFIG_PATH"
mkdir -p "$(dirname "$CONFIG_PATH")"
mv "$CONFIG_TMP" "$CONFIG_PATH"
elif [ -f "$UNPACK_DIR/config.yaml" ]; then
core_progress_command "copy config example to $WORKING_DIR/config.yaml.example"
cp "$UNPACK_DIR/config.yaml" "$WORKING_DIR/config.yaml.example" 2>/dev/null || true
fi
if [ -e "$CONFIG_PATH" ]; then
core_progress_command "$OXIDNS_BIN check -c $CONFIG_PATH -d $WORKING_DIR"
if CONFIG_CHECK_OUTPUT="$(validate_config_file "$CONFIG_PATH" 2>&1)"; then
CONFIG_CHECK_OK=1
core_progress_log "success" "$CONFIG_CHECK_OUTPUT"
else
CONFIG_CHECK_OUTPUT="${CONFIG_CHECK_OUTPUT:-configuration check failed}"
core_progress_log "error" "$CONFIG_CHECK_OUTPUT"
fi
else
CONFIG_CHECK_OUTPUT="configuration file is not installed"
core_progress_log "error" "$CONFIG_CHECK_OUTPUT"
fi
if [ "$SERVICE_WAS_RUNNING" = "1" ] && [ -x "$INIT" ]; then
SERVICE_OUT="$(mktemp "$TMP_ROOT/oxidns-service.XXXXXX")"
core_progress_command "$INIT start"
if "$INIT" start >"$SERVICE_OUT" 2>&1; then
SERVICE_RESTART="requested"
core_progress_log "success" "service restarted"
else
MSG="$(read_error_detail_file "$SERVICE_OUT" 2>/dev/null || true)"
rm -f "$SERVICE_OUT"
upload_core_error "service_restart_failed" "core installed, but service restart failed" "restart_service" "${MSG:-service restart failed}"
return 1
fi
rm -f "$SERVICE_OUT"
fi
BUILD_JSON="$(build_info_json)"
VERSION="$(json_value_from_string version "$BUILD_JSON")"
BUNDLE="$(json_value_from_string bundle "$BUILD_JSON")"
rm -f "$UPLOAD_PATH"
rm -rf "$CORE_TMP_DIR"
core_progress_log "success" "uploaded core installed: version=${VERSION:-unknown} bundle=${BUNDLE:-unknown}"
printf '{"ok":true,"action":"upload_install","kind":'
json_string "$UPLOAD_KIND"
printf ',"version":'
json_string "$VERSION"
printf ',"bundle":'
json_string "$BUNDLE"
printf ',"binary_path":'
json_string "$OXIDNS_BIN"
printf ',"webui_installed":'
json_bool "$HAS_WEBUI"
printf ',"webui_path":'
json_string "$WEBUI_DIR"
printf ',"service_restart":'
json_string "$SERVICE_RESTART"
printf ',"config_check":{"ok":'
json_bool "$CONFIG_CHECK_OK"
printf ',"message":'
json_string "$CONFIG_CHECK_OUTPUT"
printf '}}\n'
}
emit_core_upload_install_locked() {
load_settings
core_progress_reset "install uploaded OxiDNS core"
INPUT="$(cat)"
UPLOAD_PATH="$(input_value path "$INPUT")"
core_progress_log "info" "uploaded_path=${UPLOAD_PATH:-missing}"
if ! valid_upload_path "$UPLOAD_PATH" || [ ! -f "$UPLOAD_PATH" ]; then
core_progress_log "error" "invalid upload path"
json_detailed_error "invalid_upload_path" "uploaded file must be a readable /tmp/oxidns-core-upload-* file" "validate_uploaded_core" "path=${UPLOAD_PATH:-missing}"
return 1
fi
CORE_TMP_DIR="$(mktemp -d "$TMP_ROOT/oxidns-core-upload.XXXXXX")"
UNPACK_DIR="$CORE_TMP_DIR/unpack"
mkdir -p "$UNPACK_DIR"
core_progress_command "tar -tzf $UPLOAD_PATH"
if tar -tzf "$UPLOAD_PATH" >/dev/null 2>&1; then
if ! validate_safe_archive "$UPLOAD_PATH"; then
upload_core_error "uploaded_archive_unsafe" "uploaded archive contains unsafe paths or links" "validate_archive" "archive=$UPLOAD_PATH"
return 1
fi
TAR_OUT="$(mktemp "$TMP_ROOT/oxidns-upload-tar.XXXXXX")"
core_progress_command "tar -xzf $UPLOAD_PATH -C $UNPACK_DIR"
if ! tar -xzf "$UPLOAD_PATH" -C "$UNPACK_DIR" >"$TAR_OUT" 2>&1; then
TAR_DETAIL="$(read_error_detail_file "$TAR_OUT" 2>/dev/null || true)"
rm -f "$TAR_OUT"
upload_core_error "uploaded_archive_unpack_failed" "failed to unpack uploaded OxiDNS archive" "unpack_archive" "${TAR_DETAIL:-tar extraction failed}"
return 1
fi
rm -f "$TAR_OUT"
install_uploaded_core_from_unpack "archive" "$UNPACK_DIR"
return $?
fi
core_progress_command "cp $UPLOAD_PATH $UNPACK_DIR/oxidns"
cp "$UPLOAD_PATH" "$UNPACK_DIR/oxidns"
install_uploaded_core_from_unpack "binary" "$UNPACK_DIR"
}
emit_core_upload_install() {
run_with_core_lock emit_core_upload_install_locked
}
emit_core_remove_locked() {
INPUT="$(cat)"
load_settings
core_progress_reset "remove OxiDNS core"
REMOVE_CONFIG="$(input_bool remove_config "$INPUT")"
REMOVE_WORKDIR="$(input_bool remove_workdir "$INPUT")"
CONFIG_REMOVED=0
WORKDIR_REMOVED=0
CONFIG_BACKUP=""
if [ "$REMOVE_CONFIG" = "1" ] && ! safe_remove_config_path "$CONFIG_PATH"; then
core_progress_log "error" "refusing unsafe config path: $CONFIG_PATH"
json_detailed_error "unsafe_config_path" "refusing to delete unsafe configuration path" "remove_config" "config=$CONFIG_PATH"
return 1
fi
if [ "$REMOVE_WORKDIR" = "1" ] && ! safe_remove_working_dir "$WORKING_DIR"; then
core_progress_log "error" "refusing unsafe working directory: $WORKING_DIR"
json_detailed_error "unsafe_working_dir" "refusing to delete unsafe working directory" "remove_working_dir" "working_dir=$WORKING_DIR"
return 1
fi
if [ "$REMOVE_WORKDIR" = "1" ] && [ "$REMOVE_CONFIG" != "1" ] && path_is_under "$CONFIG_PATH" "$WORKING_DIR" && [ -e "$CONFIG_PATH" ]; then
CONFIG_BACKUP="$(mktemp "$TMP_ROOT/oxidns-config-preserve.XXXXXX")"
core_progress_command "preserve $CONFIG_PATH before removing $WORKING_DIR"
if cp "$CONFIG_PATH" "$CONFIG_BACKUP" 2>/dev/null; then
:
else
rm -f "$CONFIG_BACKUP"
CONFIG_BACKUP=""
core_progress_log "error" "failed to preserve config before removing working directory"
json_detailed_error "config_preserve_failed" "failed to preserve configuration before removing working directory" "remove_working_dir" "config=$CONFIG_PATH working_dir=$WORKING_DIR"
return 1
fi
fi
INIT="$(init_script)"
if [ -x "$INIT" ]; then
core_progress_command "$INIT stop"
"$INIT" stop >/dev/null 2>&1 || true
core_progress_command "$INIT disable"
"$INIT" disable >/dev/null 2>&1 || true
fi
core_progress_command "rm -f $OXIDNS_BIN"
rm -f "$OXIDNS_BIN"
core_progress_command "rm -rf $WEBUI_DIR"
rm -rf "$WEBUI_DIR"
if [ "$REMOVE_CONFIG" = "1" ]; then
core_progress_command "rm -f $CONFIG_PATH"
rm -f "$CONFIG_PATH"
CONFIG_REMOVED=1
fi
if [ "$REMOVE_WORKDIR" = "1" ]; then
core_progress_command "rm -rf $WORKING_DIR"
rm -rf "$WORKING_DIR"
WORKDIR_REMOVED=1
fi
if [ -n "$CONFIG_BACKUP" ] && [ -f "$CONFIG_BACKUP" ]; then
core_progress_command "restore preserved config to $CONFIG_PATH"
mkdir -p "$(dirname "$CONFIG_PATH")"
mv "$CONFIG_BACKUP" "$CONFIG_PATH"
fi
core_progress_log "success" "core removed"
printf '{"ok":true,"action":"remove","binary_path":'
json_string "$OXIDNS_BIN"
printf ',"webui_path":'
json_string "$WEBUI_DIR"
printf ',"config_path":'
json_string "$CONFIG_PATH"
printf ',"working_dir":'
json_string "$WORKING_DIR"
printf ',"config_removed":'
json_bool "$CONFIG_REMOVED"
printf ',"working_dir_removed":'
json_bool "$WORKDIR_REMOVED"
printf ',"config_preserved":'
if [ "$CONFIG_REMOVED" = "1" ]; then json_bool 0; else json_bool 1; fi
printf ',"working_dir_preserved":'
if [ "$WORKDIR_REMOVED" = "1" ]; then json_bool 0; else json_bool 1; fi
printf '}\n'
}
emit_core_remove() {
run_with_core_lock emit_core_remove_locked
}
case "${1:-}" in
list)
printf '{'
printf '"status":{},'
printf '"core_install":{},'
printf '"core_reinstall":{},'
printf '"core_upload_install":{"path":"string"},'
printf '"core_progress":{},'
printf '"core_remove":{"remove_config":"boolean","remove_workdir":"boolean"},'
printf '"service_start":{},'
printf '"service_stop":{},'
printf '"service_restart":{},'
printf '"service_enable":{},'
printf '"service_disable":{},'
printf '"config_read":{},'
printf '"config_validate":{"content":"string"},'
printf '"config_save":{"content":"string","base_mtime":"string","restart":"boolean"},'
printf '"settings_read":{},'
printf '"settings_save":{"core_repository":"string","core_bundle":"string","config_path":"string","working_dir":"string","download_proxy":"string","github_token":"string","clear_download_proxy":"boolean","clear_github_token":"boolean","log_syslog":"string"},'
printf '"logs_recent":{"limit":"number"}'
printf '}\n'
;;
call)
case "${2:-}" in
status)
emit_status
;;
core_install)
emit_core_install
;;
core_reinstall)
emit_core_reinstall
;;
core_upload_install)
emit_core_upload_install
;;
core_progress)
emit_core_progress
;;
core_remove)
emit_core_remove
;;
service_start)
run_service start
;;
service_stop)
run_service stop
;;
service_restart)
run_service restart
;;
service_enable)
run_service enable
;;
service_disable)
run_service disable
;;
config_read)
emit_config_read
;;
config_validate)
emit_config_validate
;;
config_save)
emit_config_save
;;
settings_read)
emit_settings_read
;;
settings_save)
emit_settings_save
;;
logs_recent)
emit_logs_recent
;;
*)
printf '{"ok":false,"error":"unknown method"}\n'
exit 1
;;
esac
;;
*)
printf '{"ok":false,"error":"usage: luci.oxidns list|call <method>"}\n'
exit 1
;;
esac