mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-29 19:05:13 +08:00
update 2026-09-06 01:51:51
This commit is contained in:
1 parent
47831f020d
commit
37eb246de8
1970 files changed
+1523980
-168
No files matched your search
Regular → Executable
+241
-62
@@ -1,71 +1,250 @@
|
||||
#!/bin/sh
|
||||
# config-backup.sh export|import — back up/restore the whole daede config
|
||||
# (dae + daed + active backend). export: prints base64(tar.gz) to stdout.
|
||||
# import: decodes /tmp/daede-import.b64, validates, restores, restarts backend.
|
||||
# Whole-plugin backup/import/reset. Only fixed configuration paths are managed.
|
||||
set -eu
|
||||
umask 077
|
||||
|
||||
ACTION="${1:-}"
|
||||
LOG="/tmp/luci-app-daede.backup.log"
|
||||
IMPORT_B64="/tmp/daede-import.b64"
|
||||
MAX_TAR=184320 # 180 KiB tar.gz -> ~240 KiB base64, under the ubus limit
|
||||
|
||||
# relative paths (no leading /) so tar entries match the extraction whitelist
|
||||
WHITELIST="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db"
|
||||
UPLOAD="${2:-/tmp/daede-import.b64}"
|
||||
LOG=/tmp/luci-app-daede.backup.log
|
||||
LOCK=/tmp/luci-app-daede.config.lock
|
||||
SHARE=/usr/share/luci-app-daede
|
||||
MAX_TAR=184320 # Base64 must fit inside the ubus reply (~240 KiB).
|
||||
MAX_RAW=16777216
|
||||
FILES="etc/config/dae etc/config/daed etc/config/daede etc/dae/config.dae etc/daed/wing.db etc/daed/wing.db-wal etc/daed/wing.db-shm"
|
||||
WORK=""
|
||||
SNAPSHOT=0
|
||||
MUTATING=0
|
||||
KEEP=0
|
||||
RESTART=""
|
||||
|
||||
fail() { echo "$*" >&2; exit 1; }
|
||||
b64enc() { ucode -e 'let f=require("fs"); print(b64enc(f.open(ARGV[0],"r").read("all")));' -- "$1"; }
|
||||
b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); o.write(r); o.close();' -- "$1" "$2"; }
|
||||
b64dec() { ucode -e 'let f=require("fs"); let r=b64dec(trim(f.open(ARGV[0],"r").read("all"))); if(!r)exit(1); let o=f.open(ARGV[1],"w"); if(!o || o.write(r)!=length(r))exit(1); o.close();' -- "$1" "$2"; }
|
||||
# Do not merge pending CLI UCI deltas into snapshots/defaults.
|
||||
config() { /sbin/uci -c /etc/config -t "$WORK/uci" "$@"; }
|
||||
|
||||
case "$ACTION" in
|
||||
export)
|
||||
exist=""
|
||||
for p in $WHITELIST; do [ -e "/$p" ] && exist="$exist $p"; done
|
||||
[ -n "$exist" ] || { echo "no config to back up" >&2; exit 1; }
|
||||
tmp="$(mktemp)"
|
||||
( cd / && tar -czf "$tmp" $exist ) 2>/dev/null || { rm -f "$tmp"; echo "tar failed" >&2; exit 1; }
|
||||
if [ "$(wc -c < "$tmp")" -gt "$MAX_TAR" ]; then
|
||||
rm -f "$tmp"; echo "config too large; use System Backup instead" >&2; exit 2
|
||||
fi
|
||||
b64enc "$tmp"
|
||||
rm -f "$tmp"
|
||||
;;
|
||||
|
||||
import)
|
||||
[ -f "$IMPORT_B64" ] || { echo "no upload found" >&2; exit 1; }
|
||||
(
|
||||
exec >"$LOG" 2>&1
|
||||
echo "$(date '+%F %T') begin import"
|
||||
rc=0
|
||||
tmp="$(mktemp)"
|
||||
if ! b64dec "$IMPORT_B64" "$tmp"; then
|
||||
echo "decode failed"; rc=1
|
||||
elif ! gzip -t "$tmp" 2>/dev/null; then
|
||||
echo "not a valid backup archive"; rc=1
|
||||
else
|
||||
# reject any entry outside the whitelist (path-traversal guard)
|
||||
bad="$(tar -tzf "$tmp" 2>/dev/null | grep -vxE 'etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config\.dae|etc/daed/wing\.db' | head -1)"
|
||||
if [ -n "$bad" ]; then
|
||||
echo "rejected: unexpected entry '$bad'"; rc=1
|
||||
else
|
||||
ab="$(uci -q get daede.config.active_backend || echo dae)"
|
||||
[ -x "/etc/init.d/$ab" ] && /etc/init.d/"$ab" stop 2>/dev/null || true
|
||||
if ( cd / && tar -xzf "$tmp" ); then
|
||||
echo "restored config"
|
||||
else
|
||||
echo "extract failed"; rc=1
|
||||
fi
|
||||
ab="$(uci -q get daede.config.active_backend || echo "$ab")"
|
||||
[ -x "/etc/init.d/$ab" ] && { /etc/init.d/"$ab" enabled || /etc/init.d/"$ab" enable; /etc/init.d/"$ab" restart 2>/dev/null || /etc/init.d/"$ab" start 2>/dev/null; } || true
|
||||
fi
|
||||
paths() {
|
||||
printf '%s\n' $FILES
|
||||
for sub in "$1"/etc/dae/subscriptions/*.sub; do
|
||||
[ -e "$sub" ] || [ -L "$sub" ] || continue
|
||||
name="${sub##*/}"
|
||||
case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'unexpected subscription filename' ;; esac
|
||||
printf 'etc/dae/subscriptions/%s\n' "$name"
|
||||
done
|
||||
}
|
||||
check_paths() {
|
||||
for dir in /etc/config /etc/dae /etc/daed /etc/dae/subscriptions; do
|
||||
[ ! -L "$dir" ] || fail "refusing symlink: $dir"
|
||||
done
|
||||
paths / > "$WORK/paths"
|
||||
while IFS= read -r p; do
|
||||
[ ! -L "/$p" ] || fail "refusing symlink: /$p"
|
||||
[ ! -e "/$p" ] || [ -f "/$p" ] || fail "not a regular file: /$p"
|
||||
done < "$WORK/paths"
|
||||
}
|
||||
snapshot() {
|
||||
mkdir -p "$WORK/before"
|
||||
while IFS= read -r p; do
|
||||
[ -f "/$p" ] || continue
|
||||
mkdir -p "$WORK/before/${p%/*}"
|
||||
cp -p "/$p" "$WORK/before/$p"
|
||||
done < "$WORK/paths"
|
||||
SNAPSHOT=1
|
||||
}
|
||||
stop_backends() {
|
||||
for svc in dae daed; do
|
||||
[ -x "/etc/init.d/$svc" ] || continue
|
||||
if /etc/init.d/"$svc" running >/dev/null 2>&1; then RESTART="$RESTART $svc"; fi
|
||||
/etc/init.d/"$svc" stop >/dev/null 2>&1 || fail "failed to stop $svc; configuration unchanged"
|
||||
done
|
||||
# procd termination can be asynchronous. Never copy a live SQLite database.
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if ! pidof dae daed daed-guard >/dev/null 2>&1; then return; fi
|
||||
sleep 1
|
||||
done
|
||||
fail 'backend still running; configuration unchanged'
|
||||
}
|
||||
disable_backends() {
|
||||
for svc in dae daed; do
|
||||
if [ -f "/etc/config/$svc" ]; then
|
||||
config -q set "$svc.config=$svc" || return 1
|
||||
config -q set "$svc.config.enabled=0" || return 1
|
||||
config -q commit "$svc" || return 1
|
||||
fi
|
||||
rm -f "$tmp" "$IMPORT_B64"
|
||||
if [ "$rc" = 0 ]; then echo "result: config restored, backend restarted"; else echo "result: import failed"; 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"
|
||||
;;
|
||||
if [ -x "/etc/init.d/$svc" ]; then
|
||||
/etc/init.d/"$svc" disable >/dev/null 2>&1 || return 1
|
||||
/etc/init.d/"$svc" stop >/dev/null 2>&1 || return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
restore_snapshot() {
|
||||
# Remove files created by the failed operation as well as original files.
|
||||
paths / > "$WORK/current" || return 1
|
||||
while IFS= read -r p; do rm -f "/$p" || return 1; done < "$WORK/current"
|
||||
while IFS= read -r p; do
|
||||
[ -f "$WORK/before/$p" ] || continue
|
||||
mkdir -p "/${p%/*}" || return 1
|
||||
cp -p "$WORK/before/$p" "/$p" || return 1
|
||||
done < "$WORK/paths"
|
||||
}
|
||||
sync_cron() {
|
||||
geo=disable; sub=disable
|
||||
[ "$(config -q get daede.config.geo_auto || :)" != 1 ] || geo=enable
|
||||
[ "$(config -q get daed.config.subscribe_auto_update || :)" != 1 ] || sub=enable
|
||||
"$SHARE/geo-cron.sh" "$geo" >/dev/null 2>&1
|
||||
"$SHARE/daed-sub-cron.sh" "$sub" >/dev/null 2>&1
|
||||
}
|
||||
finish() {
|
||||
rc=$?
|
||||
trap - EXIT HUP INT TERM
|
||||
if [ "$rc" -ne 0 ] && [ "$MUTATING" = 1 ]; then
|
||||
disable_backends || true
|
||||
if [ "$SNAPSHOT" = 1 ] && ! restore_snapshot; then
|
||||
KEEP=1
|
||||
echo "rollback failed; root-only recovery files retained at $WORK" >&2
|
||||
else
|
||||
echo 'previous configuration restored; backends remain disabled' >&2
|
||||
fi
|
||||
disable_backends || { KEEP=1; echo 'failed to disable backend; check service state' >&2; }
|
||||
sync_cron || true
|
||||
fi
|
||||
if [ "$ACTION" = export ]; then
|
||||
for svc in $RESTART; do
|
||||
/etc/init.d/"$svc" start >/dev/null 2>&1 || { rc=1; echo "failed to resume $svc" >&2; }
|
||||
done
|
||||
fi
|
||||
if [ "$ACTION" != export ]; then
|
||||
if [ "$rc" = 0 ]; then echo '✓ 完成'; else echo '✗ 失败'; fi
|
||||
fi
|
||||
[ "$KEEP" = 1 ] || rm -rf "$WORK"
|
||||
rmdir "$LOCK"
|
||||
exit "$rc"
|
||||
}
|
||||
validate_archive() {
|
||||
[ "$(wc -c < "$WORK/upload.b64")" -le 245760 ] || fail 'upload too large'
|
||||
b64dec "$WORK/upload.b64" "$WORK/import.gz" || fail 'invalid base64 upload'
|
||||
[ "$(wc -c < "$WORK/import.gz")" -le "$MAX_TAR" ] || fail 'archive too large'
|
||||
gzip -t "$WORK/import.gz" 2>/dev/null || fail 'invalid gzip archive'
|
||||
gzip -dc "$WORK/import.gz" | head -c 16777217 > "$WORK/import.tar"
|
||||
[ "$(wc -c < "$WORK/import.tar")" -le "$MAX_RAW" ] || fail 'unpacked backup exceeds 16 MiB'
|
||||
tar -tf "$WORK/import.tar" > "$WORK/entries" 2>/dev/null || fail 'invalid tar archive'
|
||||
[ -s "$WORK/entries" ] || fail 'empty archive'
|
||||
# Reject links, directories, devices and duplicate entries before extraction.
|
||||
tar -tvf "$WORK/import.tar" > "$WORK/types" 2>/dev/null || fail 'invalid tar headers'
|
||||
if grep -qv '^-' "$WORK/types"; then fail 'only regular files are allowed'; fi
|
||||
[ -z "$(sort "$WORK/entries" | uniq -d)" ] || fail 'duplicate archive entry'
|
||||
while IFS= read -r p; do
|
||||
case "$p" in
|
||||
etc/config/dae|etc/config/daed|etc/config/daede|etc/dae/config.dae|etc/daed/wing.db|etc/daed/wing.db-wal|etc/daed/wing.db-shm) ;;
|
||||
etc/dae/subscriptions/*.sub)
|
||||
name="${p#etc/dae/subscriptions/}"
|
||||
case "${name%.sub}" in ''|*[!A-Za-z0-9_]*) fail 'invalid subscription path' ;; esac ;;
|
||||
*) fail 'unexpected archive entry' ;;
|
||||
esac
|
||||
done < "$WORK/entries"
|
||||
mkdir -p "$WORK/after"
|
||||
tar -xf "$WORK/import.tar" -C "$WORK/after"
|
||||
found=0
|
||||
for svc in dae daed daede; do
|
||||
[ -f "$WORK/after/etc/config/$svc" ] || continue
|
||||
found=1
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" -q export "$svc" >/dev/null || fail "invalid $svc configuration"
|
||||
done
|
||||
[ "$found" = 1 ] || fail 'backup has no UCI configuration'
|
||||
if [ -f "$WORK/after/etc/daed/wing.db-wal" ]; then
|
||||
[ -f "$WORK/after/etc/daed/wing.db" ] || fail 'database WAL has no database'
|
||||
fi
|
||||
ab="$(/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" -q get daede.config.active_backend || echo dae)"
|
||||
case "$ab" in dae|daed) ;; *) fail 'invalid backend in backup' ;; esac
|
||||
[ -x "/etc/init.d/$ab" ] || fail "backup backend $ab is not installed"
|
||||
}
|
||||
run_action() {
|
||||
trap finish EXIT
|
||||
trap 'exit 1' HUP INT TERM
|
||||
check_paths
|
||||
if [ "$ACTION" = import ]; then validate_archive; fi
|
||||
if [ "$ACTION" = reset ]; then
|
||||
mkdir -p "$WORK/after/etc/config"
|
||||
for svc in dae daed daede; do
|
||||
cp "$SHARE/defaults/$svc" "$WORK/after/etc/config/$svc"
|
||||
done
|
||||
"$SHARE/config-defaults.sh" "$WORK/after/etc/config"
|
||||
if [ ! -x /etc/init.d/daed ]; then
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" set daede.config.active_backend=dae
|
||||
/sbin/uci -c "$WORK/after/etc/config" -t "$WORK/uci" commit daede
|
||||
fi
|
||||
fi
|
||||
stop_backends
|
||||
snapshot
|
||||
if [ "$ACTION" = export ]; then
|
||||
paths "$WORK/before" > "$WORK/candidates"
|
||||
: > "$WORK/entries"
|
||||
while IFS= read -r p; do [ ! -f "$WORK/before/$p" ] || echo "$p" >> "$WORK/entries"; done < "$WORK/candidates"
|
||||
[ -s "$WORK/entries" ] || fail 'no configuration to export'
|
||||
tar -czf "$WORK/export.gz" -C "$WORK/before" -T "$WORK/entries"
|
||||
[ "$(wc -c < "$WORK/export.gz")" -le "$MAX_TAR" ] || fail 'config too large; use System Backup instead'
|
||||
b64enc "$WORK/export.gz"
|
||||
return
|
||||
fi
|
||||
MUTATING=1
|
||||
disable_backends
|
||||
# Old backups may omit an uninstalled backend. Keep its UCI file, but remove
|
||||
# all managed data first so stale WAL/SHM and local subscriptions cannot leak in.
|
||||
while IFS= read -r p; do
|
||||
case "$p" in etc/config/*) continue ;; esac
|
||||
rm -f "/$p"
|
||||
done < "$WORK/paths"
|
||||
paths "$WORK/after" > "$WORK/replacement"
|
||||
while IFS= read -r p; do
|
||||
[ -f "$WORK/after/$p" ] || continue
|
||||
mkdir -p "/${p%/*}"
|
||||
cp "$WORK/after/$p" "/$p"
|
||||
chmod 600 "/$p"
|
||||
done < "$WORK/replacement"
|
||||
if [ "$ACTION" = reset ]; then
|
||||
disable_backends
|
||||
sync_cron
|
||||
echo 'All defaults restored. Backends are disabled; configure before starting manually.'
|
||||
else
|
||||
# Preserve the imported active backend's enabled flag. The other backend
|
||||
# stays disabled even when the archive was made on a dual-backend router.
|
||||
for svc in dae daed; do
|
||||
[ "$svc" = "$ab" ] || [ ! -f "/etc/config/$svc" ] || { config -q set "$svc.config.enabled=0"; config -q commit "$svc"; }
|
||||
done
|
||||
sync_cron
|
||||
if [ "$(config -q get "$ab.config.enabled" || :)" = 1 ]; then
|
||||
/etc/init.d/"$ab" enable
|
||||
/etc/init.d/"$ab" start
|
||||
/etc/init.d/"$ab" running >/dev/null 2>&1 || fail 'restored backend failed to start'
|
||||
else
|
||||
/etc/init.d/"$ab" disable
|
||||
fi
|
||||
echo 'Backup restored. Active backend follows the saved enabled setting.'
|
||||
fi
|
||||
}
|
||||
|
||||
*)
|
||||
echo "usage: $0 export|import" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
case "$ACTION" in export|import|reset) ;; *) fail "usage: $0 export|import [upload]|reset" ;; esac
|
||||
# An atomic lock covers the complete operation, including background execution.
|
||||
mkdir "$LOCK" 2>/dev/null || fail 'another configuration operation is running'
|
||||
WORK="$(mktemp -d /tmp/daede-config.XXXXXX)" || { rmdir "$LOCK"; exit 1; }
|
||||
mkdir -p "$WORK/uci"
|
||||
if [ "$ACTION" = import ]; then
|
||||
case "$UPLOAD" in
|
||||
/tmp/daede-import.b64) ;;
|
||||
/tmp/daede-import.*.b64)
|
||||
token="${UPLOAD#/tmp/daede-import.}"; token="${token%.b64}"
|
||||
case "$token" in ''|*[!a-f0-9]*) rm -rf "$WORK"; rmdir "$LOCK"; fail 'invalid upload path' ;; esac ;;
|
||||
*) rm -rf "$WORK"; rmdir "$LOCK"; fail 'invalid upload path' ;;
|
||||
esac
|
||||
if [ ! -f "$UPLOAD" ] || [ -L "$UPLOAD" ]; then rm -rf "$WORK"; rmdir "$LOCK"; fail 'upload missing or unsafe'; fi
|
||||
mv "$UPLOAD" "$WORK/upload.b64" || { rm -rf "$WORK"; rmdir "$LOCK"; exit 1; }
|
||||
fi
|
||||
if [ "$ACTION" = export ]; then
|
||||
run_action
|
||||
else
|
||||
# Truncate before returning, so the frontend cannot mistake an earlier job
|
||||
# for this one. The child owns the lock and all cleanup from here on.
|
||||
: > "$LOG"
|
||||
(run_action) </dev/null >"$LOG" 2>&1 &
|
||||
echo "started in background, see $LOG"
|
||||
fi
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
# Shared installation/reset defaults. Optional directory is an isolated UCI tree.
|
||||
set -eu
|
||||
if [ "$#" -gt 0 ]; then
|
||||
config_dir="$1"
|
||||
mkdir -p "$config_dir/.uci"
|
||||
uci() { /sbin/uci -c "$config_dir" -t "$config_dir/.uci" "$@"; }
|
||||
fi
|
||||
|
||||
if ! uci -q get dae.config.lan_interface >/dev/null 2>&1; then
|
||||
uci -q set dae.config.lan_interface='br-lan'
|
||||
fi
|
||||
if ! uci -q show dae | grep -q "=group$"; then
|
||||
g="$(uci add dae group)"
|
||||
uci -q set "dae.$g.name=proxy"
|
||||
uci -q set "dae.$g.policy=min_moving_avg"
|
||||
fi
|
||||
if ! uci -q get dae.routing >/dev/null 2>&1; then
|
||||
uci -q set dae.routing=routing
|
||||
uci -q set dae.routing.private_direct=1
|
||||
uci -q set dae.routing.cn_direct=1
|
||||
uci -q set dae.routing.block_ads=0
|
||||
uci -q set dae.routing.fallback=proxy
|
||||
fi
|
||||
if ! uci -q get dae.dns >/dev/null 2>&1; then
|
||||
uci -q set dae.dns=dns
|
||||
uci -q set dae.dns.cn_upstream='udp://dns.alidns.com:53'
|
||||
uci -q set dae.dns.fallback_upstream='tcp+udp://dns.google:53'
|
||||
uci -q set dae.dns.response_ttl='0'
|
||||
fi
|
||||
uci -q commit dae
|
||||
@@ -30,8 +30,7 @@
|
||||
"/usr/share/luci-app-daede/pkg-info.sh daed": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh luci-app-daede": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/refresh-index.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ]
|
||||
"/usr/share/luci-app-daede/proxy-check.sh": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
@@ -83,7 +82,11 @@
|
||||
"/usr/share/luci-app-daede/gen-dae-config.sh import": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-update.sh": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-cron.sh enable": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/daed-sub-cron.sh disable": [ "exec" ]
|
||||
"/usr/share/luci-app-daede/daed-sub-cron.sh disable": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh export": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh reset": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/config-backup.sh import /tmp/daede-import.*.b64": [ "exec" ],
|
||||
"/tmp/daede-import.*.b64": [ "write" ]
|
||||
},
|
||||
"uci": [ "dae", "daed", "daede" ]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user