#!/usr/bin/env bash set -Eeuo pipefail REPO="${NG_REPO:-a2899882/NexusGate}" BRANCH="${NG_BRANCH:-main}" die() { printf '错误:%s\n' "$*" >&2; exit 1; } info() { printf '\033[1;36m[NexusGate]\033[0m %s\n' "$*"; } need_root() { [[ "${EUID}" -eq 0 ]] || die "请使用 root 运行"; } IS_OPENWRT=false [[ -f /etc/openwrt_release ]] && IS_OPENWRT=true if $IS_OPENWRT; then DATA_DIR=/etc/nexusgate/data; else DATA_DIR=/var/lib/nexusgate; fi panel_stop() { if $IS_OPENWRT; then /etc/init.d/nexusgate stop; else systemctl stop nexusgate; fi } panel_start() { if $IS_OPENWRT; then /etc/init.d/nexusgate start; else systemctl start nexusgate; fi } panel_restart() { if $IS_OPENWRT; then /etc/init.d/nexusgate restart; else systemctl restart nexusgate; fi } panel_status() { if $IS_OPENWRT; then /etc/init.d/nexusgate status; else systemctl status nexusgate --no-pager; fi } panel_logs() { local n="${1:-120}"; if $IS_OPENWRT; then logread 2>/dev/null | grep -a nexusgate | tail -n "$n"; else journalctl -u nexusgate -n "$n" --no-pager; fi } caddy_reload() { if $IS_OPENWRT; then /etc/init.d/caddy reload; else systemctl reload caddy; fi } own_data() { if ! $IS_OPENWRT; then chown -R nexusgate:nexusgate "$DATA_DIR"; chmod 0700 "$DATA_DIR"; fi } backup() { need_root local output="${1:-/root/nexusgate-backup-$(date +%Y%m%d-%H%M%S).tar.gz}" local stage stage="$(mktemp -d /tmp/nexusgate-backup.XXXXXX)" trap 'rm -rf -- "$stage"' RETURN cp -a "$DATA_DIR" "$stage/data" cp -a /etc/nexusgate.env "$stage/nexusgate.env" [[ -f /etc/caddy/Caddyfile.d/nexusgate.caddy ]] && cp -a /etc/caddy/Caddyfile.d/nexusgate.caddy "$stage/nexusgate.caddy" tar -czf "$output" -C "$stage" . chmod 0600 "$output" info "备份已生成:$output" } restore() { need_root local archive="${1:-}" if [[ -z "$archive" ]]; then archive="$(find /root -maxdepth 1 -type f -name 'nexusgate-backup-*.tar.gz' -printf '%T@ %p\n' | sort -nr | head -n1 | cut -d' ' -f2-)"; fi [[ -f "$archive" ]] || die "未找到备份文件" if tar -tzf "$archive" | grep -Eq '(^/|(^|/)\.\.(/|$))'; then die "备份包包含不安全路径"; fi local stage safety stage="$(mktemp -d /tmp/nexusgate-restore.XXXXXX)" trap 'rm -rf -- "$stage"' RETURN tar -xzf "$archive" -C "$stage" [[ -f "$stage/data/nexusgate.json" && -f "$stage/nexusgate.env" ]] || die "备份包不完整" safety="/root/nexusgate-before-restore-$(date +%Y%m%d-%H%M%S).tar.gz" backup "$safety" panel_stop find "$DATA_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + cp -a "$stage/data/." "$DATA_DIR/" own_data cp -a "$stage/nexusgate.env" /etc/nexusgate.env && chmod 0600 /etc/nexusgate.env if [[ -f "$stage/nexusgate.caddy" ]]; then cp -a "$stage/nexusgate.caddy" /etc/caddy/Caddyfile.d/nexusgate.caddy; fi panel_start caddy_reload || true info "恢复完成;恢复前快照:$safety" } update_panel() { need_root local stage current_backup old_backup local -a old_backups=() current_backup="/root/nexusgate-before-update-$(date +%Y%m%d-%H%M%S).tar.gz" backup "$current_backup" stage="$(mktemp -d /tmp/nexusgate-update.XXXXXX)" trap 'rm -rf -- "$stage"' RETURN curl -fL --retry 3 "https://github.com/${REPO}/archive/refs/heads/${BRANCH}.tar.gz" -o "$stage/source.tgz" mkdir -p "$stage/source" && tar -xzf "$stage/source.tgz" -C "$stage/source" --strip-components=1 [[ -f "$stage/source/server.js" ]] || die "更新包不完整" panel_stop cp -a "$stage/source/." /opt/nexusgate/ if $IS_OPENWRT; then install -m 0755 /opt/nexusgate/openrc/nexusgate /etc/init.d/nexusgate else install -m 0644 /opt/nexusgate/systemd/nexusgate.service /etc/systemd/system/nexusgate.service systemctl daemon-reload fi install -m 0755 /opt/nexusgate/scripts/nexusgate.sh /usr/local/sbin/nexusgate own_data [[ ! -f "$DATA_DIR/nexusgate.json" ]] || chmod 0600 "$DATA_DIR/nexusgate.json" panel_start local healthy=false for _ in {1..30}; do if curl -fsS http://127.0.0.1:8787/healthz >/dev/null 2>&1; then healthy=true; break; fi sleep 1 done if [[ "$healthy" != true ]]; then panel_logs 35 || true die "更新后控制面启动失败;可使用上方备份恢复" fi if [[ -f /etc/nexusgate/agent.env ]]; then info '检测到本机同时承载节点,更新本机 Agent' bash "$stage/source/scripts/agent-update.sh" || info 'Agent 更新失败;控制面仍可运行,请执行 ng-agent doctor 查看原因' fi # Only automatic pre-update snapshots are pruned; manual and pre-restore # backups remain untouched. Keep five successful rollback points on disk. mapfile -t old_backups < <(find /root -maxdepth 1 -type f -name 'nexusgate-before-update-*.tar.gz' -printf '%T@ %p\n' | sort -nr | sed -n '6,$p' | cut -d' ' -f2-) for old_backup in "${old_backups[@]}"; do rm -f -- "$old_backup"; done info "更新完成;更新前备份:$current_backup" } change_domain() { need_root local domain="${1:-}" if [[ -z "$domain" && -r /dev/tty ]]; then read -r -p '新域名:' domain /dev/null | grep -a caddy | tail -n 30; else journalctl -u caddy -n 30 --no-pager; fi } change_account() { need_root local current="${1:-}" next='' first='' second='' if [[ -r /dev/tty ]]; then read -r -p '新管理员账号(留空则保持原账号):' next /dev/null 2>&1 || true /etc/init.d/nexusgate disable >/dev/null 2>&1 || true rm -f -- /etc/init.d/nexusgate else systemctl disable --now nexusgate.service >/dev/null 2>&1 || true rm -f -- /etc/systemd/system/nexusgate.service systemctl daemon-reload userdel nexusgate >/dev/null 2>&1 || true groupdel nexusgate >/dev/null 2>&1 || true fi rm -f -- /etc/nexusgate.env /etc/caddy/Caddyfile.d/nexusgate.caddy /usr/local/sbin/ng /usr/local/sbin/nexusgate rm -rf -- /opt/nexusgate "$DATA_DIR" caddy_reload >/dev/null 2>&1 || true printf '\nNexusGate 已卸载。可恢复备份:%s\n' "$archive" } menu() { printf '\nNexusGate 管理菜单\n' printf '1. 一键升级\n2. 更换域名\n3. 检查证书\n4. 生成迁移备份\n5. 恢复迁移备份\n6. 修改管理员账号 / 密码\n7. 查看状态\n8. 重启服务\n9. 查看日志\n10. 卸载面板\n0. 退出\n' local choice read -r -p '请选择:' choice