MirrorBox Auto-Init: - Add blockchain-like gigogne P2P structure with peer zero (P0) genesis - Auto-create self-mesh on page load with configurable depth - Preserve MirrorBox peers during refresh cycles Self-Recovery System: - Add secubox-restore script for bootstrapping new OpenWrt boxes - Generate customized bootstrap.sh in Gitea backups - Support interactive and command-line restore modes ACL Fixes: - Add missing deploy/pull methods to luci-app-secubox-p2p ACL - Add luci.gitea and luci.secubox-p2p access to luci-app-secubox ACL - Fix null display issue in hub.js (changed to empty string) Backup Enhancements: - Fix syntax error in RPCD heredoc (openwrt_version line) - Add branch reference to Gitea API calls (main branch) - Include bootstrap.sh and secubox-restore in backup push Documentation: - Add comprehensive README.md for SecuBox P2P module Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
447 lines
10 KiB
Bash
447 lines
10 KiB
Bash
#!/bin/sh
|
|
#
|
|
# SecuBox Self-Recovery Bootstrap Script
|
|
# Downloads and restores a SecuBox appliance configuration from Gitea
|
|
#
|
|
# Usage:
|
|
# curl -sL http://gitea-server/user/repo/raw/branch/master/bootstrap.sh | sh
|
|
# # OR
|
|
# wget -qO- http://gitea-server/user/repo/raw/branch/master/bootstrap.sh | sh
|
|
# # OR
|
|
# secubox-restore <gitea-url> <repo-owner> <repo-name> [access-token]
|
|
#
|
|
|
|
set -e
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
|
|
print_banner() {
|
|
cat << 'EOF'
|
|
____ ____
|
|
/ ___| ___ ___ _ | __ ) _____ __
|
|
\___ \ / _ \/ __| | | | _ \ / _ \ \/ /
|
|
___) | __/ (__| |_| | |_) | (_) > <
|
|
|____/ \___|\___|\__,_|____/ \___/_/\_\
|
|
|
|
Self-Recovery Bootstrap v1.0
|
|
EOF
|
|
}
|
|
|
|
# Check if we're on OpenWrt
|
|
check_openwrt() {
|
|
if [ ! -f /etc/openwrt_release ]; then
|
|
log_error "This script must be run on OpenWrt"
|
|
exit 1
|
|
fi
|
|
. /etc/openwrt_release
|
|
log_info "Detected OpenWrt $DISTRIB_RELEASE on $(cat /tmp/sysinfo/model 2>/dev/null || echo 'unknown device')"
|
|
}
|
|
|
|
# Install dependencies if missing
|
|
install_deps() {
|
|
log_info "Checking dependencies..."
|
|
|
|
# Required packages
|
|
for pkg in curl git-http jsonfilter; do
|
|
if ! command -v "$pkg" >/dev/null 2>&1 && ! opkg list-installed | grep -q "^$pkg "; then
|
|
log_info "Installing $pkg..."
|
|
opkg update >/dev/null 2>&1 || true
|
|
opkg install "$pkg" 2>/dev/null || log_warn "Could not install $pkg"
|
|
fi
|
|
done
|
|
|
|
log_success "Dependencies ready"
|
|
}
|
|
|
|
# Fetch manifest from Gitea repository
|
|
fetch_manifest() {
|
|
local server_url="$1"
|
|
local repo_owner="$2"
|
|
local repo_name="$3"
|
|
local token="$4"
|
|
local branch="${5:-master}"
|
|
|
|
log_info "Fetching backup manifest..."
|
|
|
|
local manifest_url="${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents/manifest.json?ref=${branch}"
|
|
|
|
if [ -n "$token" ]; then
|
|
response=$(curl -sL "$manifest_url" -H "Authorization: token $token")
|
|
else
|
|
response=$(curl -sL "$manifest_url")
|
|
fi
|
|
|
|
# Check for error
|
|
if echo "$response" | jsonfilter -e '@.message' 2>/dev/null | grep -qi "not found"; then
|
|
log_error "Repository or manifest not found"
|
|
return 1
|
|
fi
|
|
|
|
# Decode base64 content
|
|
content=$(echo "$response" | jsonfilter -e '@.content' 2>/dev/null | base64 -d 2>/dev/null)
|
|
|
|
if [ -z "$content" ]; then
|
|
log_error "Failed to fetch manifest"
|
|
return 1
|
|
fi
|
|
|
|
echo "$content"
|
|
}
|
|
|
|
# Fetch and restore a file from Gitea
|
|
fetch_file() {
|
|
local server_url="$1"
|
|
local repo_owner="$2"
|
|
local repo_name="$3"
|
|
local file_path="$4"
|
|
local dest_path="$5"
|
|
local token="$6"
|
|
local branch="${7:-master}"
|
|
|
|
local file_url="${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents/${file_path}?ref=${branch}"
|
|
|
|
if [ -n "$token" ]; then
|
|
response=$(curl -sL "$file_url" -H "Authorization: token $token")
|
|
else
|
|
response=$(curl -sL "$file_url")
|
|
fi
|
|
|
|
# Check if it's a file (has content) or directory
|
|
content=$(echo "$response" | jsonfilter -e '@.content' 2>/dev/null)
|
|
|
|
if [ -n "$content" ]; then
|
|
# Single file - decode and save
|
|
mkdir -p "$(dirname "$dest_path")"
|
|
echo "$content" | base64 -d > "$dest_path" 2>/dev/null
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Fetch directory contents recursively
|
|
fetch_directory() {
|
|
local server_url="$1"
|
|
local repo_owner="$2"
|
|
local repo_name="$3"
|
|
local dir_path="$4"
|
|
local dest_base="$5"
|
|
local token="$6"
|
|
local branch="${7:-master}"
|
|
|
|
local dir_url="${server_url}/api/v1/repos/${repo_owner}/${repo_name}/contents/${dir_path}?ref=${branch}"
|
|
|
|
if [ -n "$token" ]; then
|
|
response=$(curl -sL "$dir_url" -H "Authorization: token $token")
|
|
else
|
|
response=$(curl -sL "$dir_url")
|
|
fi
|
|
|
|
# Parse each entry
|
|
echo "$response" | jsonfilter -e '@[*].path' 2>/dev/null | while read -r path; do
|
|
type=$(echo "$response" | jsonfilter -e "@[@.path='$path'].type" 2>/dev/null)
|
|
|
|
if [ "$type" = "file" ]; then
|
|
local rel_path="${path#$dir_path/}"
|
|
local dest_path="${dest_base}/${rel_path}"
|
|
fetch_file "$server_url" "$repo_owner" "$repo_name" "$path" "$dest_path" "$token" "$branch"
|
|
elif [ "$type" = "dir" ]; then
|
|
fetch_directory "$server_url" "$repo_owner" "$repo_name" "$path" "$dest_base" "$token" "$branch"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Restore configs from backup
|
|
restore_configs() {
|
|
local backup_dir="$1"
|
|
local configs_dir="$backup_dir/configs"
|
|
|
|
if [ ! -d "$configs_dir" ]; then
|
|
log_warn "No configs directory found in backup"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Restoring UCI configurations..."
|
|
|
|
local count=0
|
|
for cfg in "$configs_dir"/*; do
|
|
[ -f "$cfg" ] || continue
|
|
local name=$(basename "$cfg")
|
|
|
|
# Skip system-critical configs unless explicitly requested
|
|
case "$name" in
|
|
network|wireless|firewall)
|
|
log_warn "Skipping critical config: $name (use --include-network to restore)"
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
cp "$cfg" "/etc/config/$name"
|
|
count=$((count + 1))
|
|
done
|
|
|
|
log_success "Restored $count configuration files"
|
|
}
|
|
|
|
# Restore scripts
|
|
restore_scripts() {
|
|
local backup_dir="$1"
|
|
local scripts_dir="$backup_dir/scripts"
|
|
|
|
if [ ! -d "$scripts_dir" ]; then
|
|
return 0
|
|
fi
|
|
|
|
log_info "Restoring custom scripts..."
|
|
|
|
mkdir -p /usr/share/secubox/scripts
|
|
cp -r "$scripts_dir"/* /usr/share/secubox/scripts/ 2>/dev/null
|
|
chmod +x /usr/share/secubox/scripts/* 2>/dev/null
|
|
|
|
log_success "Scripts restored"
|
|
}
|
|
|
|
# Restore cron jobs
|
|
restore_cron() {
|
|
local backup_dir="$1"
|
|
local cron_file="$backup_dir/cron/root"
|
|
|
|
if [ ! -f "$cron_file" ]; then
|
|
return 0
|
|
fi
|
|
|
|
log_info "Restoring cron jobs..."
|
|
|
|
# Append to existing crontab (don't overwrite)
|
|
cat "$cron_file" >> /etc/crontabs/root 2>/dev/null
|
|
/etc/init.d/cron restart 2>/dev/null
|
|
|
|
log_success "Cron jobs restored"
|
|
}
|
|
|
|
# Install SecuBox packages if not present
|
|
install_secubox() {
|
|
log_info "Checking SecuBox installation..."
|
|
|
|
# Check if secubox-core is installed
|
|
if ! opkg list-installed | grep -q "secubox-core"; then
|
|
log_info "SecuBox not installed, attempting installation..."
|
|
|
|
# Try to add SecuBox feed
|
|
if ! grep -q "secubox" /etc/opkg/customfeeds.conf 2>/dev/null; then
|
|
log_warn "SecuBox feed not configured"
|
|
log_info "Please configure SecuBox feed manually and re-run this script"
|
|
return 1
|
|
fi
|
|
|
|
opkg update
|
|
opkg install secubox-core luci-app-secubox
|
|
fi
|
|
|
|
log_success "SecuBox is installed"
|
|
}
|
|
|
|
# Main restore function
|
|
do_restore() {
|
|
local server_url="$1"
|
|
local repo_owner="$2"
|
|
local repo_name="$3"
|
|
local token="$4"
|
|
local branch="${5:-master}"
|
|
|
|
# Create temp directory
|
|
local backup_dir="/tmp/secubox-restore-$$"
|
|
mkdir -p "$backup_dir"
|
|
|
|
# Fetch manifest first
|
|
manifest=$(fetch_manifest "$server_url" "$repo_owner" "$repo_name" "$token" "$branch")
|
|
|
|
if [ -z "$manifest" ]; then
|
|
log_error "Failed to fetch backup manifest"
|
|
rm -rf "$backup_dir"
|
|
return 1
|
|
fi
|
|
|
|
# Display backup info
|
|
local backup_hostname=$(echo "$manifest" | jsonfilter -e '@.hostname' 2>/dev/null)
|
|
local backup_timestamp=$(echo "$manifest" | jsonfilter -e '@.timestamp' 2>/dev/null)
|
|
local backup_version=$(echo "$manifest" | jsonfilter -e '@.secubox_version' 2>/dev/null)
|
|
|
|
echo ""
|
|
log_info "Backup Information:"
|
|
echo " Source Hostname: $backup_hostname"
|
|
echo " Backup Date: $backup_timestamp"
|
|
echo " SecuBox Version: $backup_version"
|
|
echo ""
|
|
|
|
# Ask for confirmation if interactive
|
|
if [ -t 0 ]; then
|
|
printf "Proceed with restore? [y/N] "
|
|
read -r confirm
|
|
case "$confirm" in
|
|
[yY][eE][sS]|[yY]) ;;
|
|
*) log_info "Restore cancelled"; rm -rf "$backup_dir"; return 0 ;;
|
|
esac
|
|
fi
|
|
|
|
# Fetch each component
|
|
log_info "Downloading backup components..."
|
|
|
|
for component in configs profiles presets manifests scripts cron ssh certificates; do
|
|
log_info "Fetching $component..."
|
|
fetch_directory "$server_url" "$repo_owner" "$repo_name" "$component" "$backup_dir/$component" "$token" "$branch" 2>/dev/null || true
|
|
done
|
|
|
|
# Perform restore
|
|
log_info "Applying backup..."
|
|
|
|
restore_configs "$backup_dir"
|
|
restore_scripts "$backup_dir"
|
|
restore_cron "$backup_dir"
|
|
|
|
# Save Gitea config for future backups
|
|
uci set secubox-p2p.gitea=gitea
|
|
uci set secubox-p2p.gitea.enabled='1'
|
|
uci set secubox-p2p.gitea.server_url="$server_url"
|
|
uci set secubox-p2p.gitea.repo_owner="$repo_owner"
|
|
uci set secubox-p2p.gitea.repo_name="$repo_name"
|
|
[ -n "$token" ] && uci set secubox-p2p.gitea.access_token="$token"
|
|
uci commit secubox-p2p
|
|
|
|
# Cleanup
|
|
rm -rf "$backup_dir"
|
|
|
|
log_success "Restore completed!"
|
|
echo ""
|
|
log_info "Please reboot to apply all changes: reboot"
|
|
}
|
|
|
|
# Interactive mode - prompt for settings
|
|
interactive_mode() {
|
|
print_banner
|
|
echo ""
|
|
|
|
check_openwrt
|
|
install_deps
|
|
|
|
echo ""
|
|
log_info "Enter Gitea repository details:"
|
|
echo ""
|
|
|
|
printf "Gitea Server URL (e.g., http://192.168.1.1:3000): "
|
|
read -r server_url
|
|
|
|
printf "Repository Owner (username): "
|
|
read -r repo_owner
|
|
|
|
printf "Repository Name: "
|
|
read -r repo_name
|
|
|
|
printf "Access Token (leave empty for public repos): "
|
|
read -r token
|
|
|
|
echo ""
|
|
do_restore "$server_url" "$repo_owner" "$repo_name" "$token"
|
|
}
|
|
|
|
# Show usage
|
|
usage() {
|
|
cat << EOF
|
|
SecuBox Self-Recovery Bootstrap Script
|
|
|
|
Usage:
|
|
$0 [options] <server-url> <repo-owner> <repo-name> [access-token]
|
|
$0 --interactive
|
|
|
|
Options:
|
|
--interactive, -i Run in interactive mode with prompts
|
|
--branch, -b Git branch to restore from (default: master)
|
|
--include-network Also restore network/wireless/firewall configs
|
|
--help, -h Show this help message
|
|
|
|
Examples:
|
|
# Interactive mode
|
|
$0 -i
|
|
|
|
# Direct restore from public repo
|
|
$0 http://gitea.local:3000 admin secubox-backup
|
|
|
|
# Restore with token
|
|
$0 http://gitea.local:3000 admin secubox-backup abc123token
|
|
|
|
# Restore from specific branch
|
|
$0 -b dev http://gitea.local:3000 admin secubox-backup
|
|
|
|
Quick Bootstrap (paste into new OpenWrt shell):
|
|
wget -qO- http://your-gitea/user/repo/raw/master/bootstrap.sh | sh
|
|
|
|
EOF
|
|
}
|
|
|
|
# Parse arguments
|
|
BRANCH="master"
|
|
INCLUDE_NETWORK=0
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--interactive|-i)
|
|
interactive_mode
|
|
exit 0
|
|
;;
|
|
--branch|-b)
|
|
BRANCH="$2"
|
|
shift 2
|
|
;;
|
|
--include-network)
|
|
INCLUDE_NETWORK=1
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
usage
|
|
exit 0
|
|
;;
|
|
-*)
|
|
log_error "Unknown option: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# If no arguments, run interactive
|
|
if [ $# -eq 0 ]; then
|
|
interactive_mode
|
|
exit 0
|
|
fi
|
|
|
|
# Validate arguments
|
|
if [ $# -lt 3 ]; then
|
|
log_error "Missing required arguments"
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
SERVER_URL="$1"
|
|
REPO_OWNER="$2"
|
|
REPO_NAME="$3"
|
|
ACCESS_TOKEN="${4:-}"
|
|
|
|
print_banner
|
|
echo ""
|
|
check_openwrt
|
|
install_deps
|
|
do_restore "$SERVER_URL" "$REPO_OWNER" "$REPO_NAME" "$ACCESS_TOKEN" "$BRANCH"
|