#!/bin/sh # SecuBox Feed Manager # Manages local and remote package feeds for SecuBox appstore # Copyright 2026 CyberMind FEED_DIR="/www/secubox-feed" OPKG_LIST="/var/opkg-lists/secubox" REMOTE_FEED_URL="${SECUBOX_FEED_URL:-https://feed.maegia.tv/secubox-feed}" GITEA_API="${SECUBOX_GITEA_API:-}" GITHUB_REPO="${SECUBOX_GITHUB_REPO:-}" usage() { cat << EOF SecuBox Feed Manager - Manage local and remote package feeds Usage: secubox-feed [options] Commands: update Update local feed from installed IPKs sync Sync feed to /var/opkg-lists for opkg fetch Fetch IPK from URL and add to local feed fetch-release Fetch latest release from GitHub/Gitea list List packages in local feed info Show package info install Install package from local feed install all Install all packages from local feed clean Remove old package versions serve Start simple HTTP server for WAN access export Export feed URL for sharing P2P Commands: peers List mesh peers with active package feeds search Search for package across local and all peers fetch-peer Fetch package from specific peer fetch-any Fetch package from first available peer sync-peers Sync package catalogs from all mesh peers sync-content Auto-install content packages (sites/apps) from peers Options: -h, --help Show this help -v, --verbose Verbose output -q, --quiet Quiet mode Environment: SECUBOX_FEED_URL Remote feed URL (default: $REMOTE_FEED_URL) SECUBOX_GITEA_API Gitea API URL for releases SECUBOX_GITHUB_REPO GitHub repo (owner/repo) for releases EOF } log() { [ "$QUIET" = "1" ] && return echo "$@" } verbose() { [ "$VERBOSE" = "1" ] && echo "$@" } error() { echo "Error: $@" >&2 } # Regenerate Packages index from IPK files regenerate_packages() { local feed_dir="${1:-$FEED_DIR}" log "Regenerating Packages index..." rm -f "$feed_dir/Packages" "$feed_dir/Packages.gz" local count=0 for pkg in "$feed_dir"/*.ipk; do [ -f "$pkg" ] || continue local tmp_dir=$(mktemp -d) ( cd "$tmp_dir" tar -xzf "$pkg" 2>/dev/null || return 1 tar -xzf control.tar.gz 2>/dev/null || return 1 if [ -f control ]; then # Strip libc and blank lines sed -e 's/^Depends: libc$//' \ -e 's/^Depends: libc, /Depends: /' \ -e '/^$/d' control echo "Filename: $(basename "$pkg")" echo "Size: $(stat -c%s "$pkg" 2>/dev/null || wc -c < "$pkg")" echo "SHA256sum: $(sha256sum "$pkg" | cut -d' ' -f1)" echo "" fi ) >> "$feed_dir/Packages" rm -rf "$tmp_dir" count=$((count + 1)) done gzip -kf "$feed_dir/Packages" 2>/dev/null log "Generated index for $count packages" } # Sync feed to opkg lists sync_feed() { if [ -f "$FEED_DIR/Packages" ]; then cp "$FEED_DIR/Packages" "$OPKG_LIST" log "Feed synced to $OPKG_LIST" log "$(grep -c '^Package:' "$OPKG_LIST") packages available" else error "No Packages file found. Run 'secubox-feed update' first." return 1 fi } # Fetch IPK from URL fetch_ipk() { local url="$1" local filename=$(basename "$url") log "Fetching $filename..." if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$FEED_DIR/$filename" "$url" elif command -v wget >/dev/null 2>&1; then wget -q -O "$FEED_DIR/$filename" "$url" else error "Neither curl nor wget available" return 1 fi if [ -f "$FEED_DIR/$filename" ]; then log "Downloaded: $filename" regenerate_packages sync_feed else error "Download failed" return 1 fi } # Fetch latest release from GitHub fetch_github_release() { local pkg="$1" local repo="${GITHUB_REPO:-secubox/secubox-openwrt}" log "Fetching latest release for $pkg from GitHub..." local api_url="https://api.github.com/repos/$repo/releases/latest" local release_json=$(curl -fsSL "$api_url" 2>/dev/null) if [ -z "$release_json" ]; then error "Failed to fetch release info" return 1 fi # Find IPK matching package name local ipk_url=$(echo "$release_json" | jsonfilter -e "@.assets[*].browser_download_url" 2>/dev/null | grep -i "${pkg}.*\.ipk" | head -1) if [ -n "$ipk_url" ]; then fetch_ipk "$ipk_url" else error "No IPK found for $pkg in latest release" return 1 fi } # Fetch from Gitea releases fetch_gitea_release() { local pkg="$1" if [ -z "$GITEA_API" ]; then error "SECUBOX_GITEA_API not set" return 1 fi log "Fetching latest release for $pkg from Gitea..." local api_url="$GITEA_API/repos/secubox/secubox-openwrt/releases/latest" local release_json=$(curl -fsSL "$api_url" 2>/dev/null) if [ -z "$release_json" ]; then error "Failed to fetch release info from Gitea" return 1 fi local ipk_url=$(echo "$release_json" | jsonfilter -e "@.assets[*].browser_download_url" 2>/dev/null | grep -i "${pkg}.*\.ipk" | head -1) if [ -n "$ipk_url" ]; then fetch_ipk "$ipk_url" else error "No IPK found for $pkg in Gitea release" return 1 fi } # List packages in feed list_packages() { if [ -f "$FEED_DIR/Packages" ]; then grep "^Package:" "$FEED_DIR/Packages" | sed 's/Package: //' | sort else error "No Packages file found" return 1 fi } # Show package info package_info() { local pkg="$1" if [ -f "$FEED_DIR/Packages" ]; then awk -v pkg="$pkg" ' /^Package:/ { found = ($2 == pkg) } found { print } found && /^$/ { exit } ' "$FEED_DIR/Packages" else error "No Packages file found" return 1 fi } # Install package from local feed (handles dependencies) install_package() { local pkg="$1" # Handle "all" - install all packages if [ "$pkg" = "all" ]; then install_all_packages return $? fi local ipk_pattern="$FEED_DIR/${pkg}_"*.ipk # Find the IPK file local found=$(ls $ipk_pattern 2>/dev/null | head -1) if [ -z "$found" ]; then error "Package $pkg not found in local feed" return 1 fi log "Installing $pkg..." # Get dependencies from the package local deps=$(tar -xOzf "$found" control.tar.gz 2>/dev/null | tar -xOzf - ./control 2>/dev/null | grep "^Depends:" | sed 's/^Depends: //') # Install local dependencies first if [ -n "$deps" ]; then for dep in $(echo "$deps" | tr ',' '\n' | sed 's/^ *//; s/ *$//; s/ (.*)//' | sort -u); do [ -z "$dep" ] && continue local dep_ipk=$(ls "$FEED_DIR/${dep}_"*.ipk 2>/dev/null | head -1) if [ -n "$dep_ipk" ]; then verbose "Installing dependency: $dep" opkg install "$dep_ipk" 2>/dev/null || true fi done fi # Install the main package opkg install "$found" } # Install all packages from local feed install_all_packages() { log "Installing all packages from local feed..." local total=0 local installed=0 local failed=0 # First pass: install secubox-core and dependencies for ipk in "$FEED_DIR"/secubox-core_*.ipk "$FEED_DIR"/secubox-app_*.ipk; do [ -f "$ipk" ] || continue total=$((total + 1)) local name=$(basename "$ipk" | sed 's/_[0-9].*//') log " Installing $name..." if opkg install "$ipk" 2>/dev/null; then installed=$((installed + 1)) else failed=$((failed + 1)) fi done # Second pass: install secubox-app-* backend packages for ipk in "$FEED_DIR"/secubox-app-*.ipk; do [ -f "$ipk" ] || continue # Skip secubox-app-bonus (meta package) echo "$ipk" | grep -q "secubox-app-bonus" && continue total=$((total + 1)) local name=$(basename "$ipk" | sed 's/_[0-9].*//') log " Installing $name..." if opkg install "$ipk" 2>/dev/null; then installed=$((installed + 1)) else failed=$((failed + 1)) fi done # Third pass: install luci-app-* frontend packages for ipk in "$FEED_DIR"/luci-app-*.ipk; do [ -f "$ipk" ] || continue total=$((total + 1)) local name=$(basename "$ipk" | sed 's/_[0-9].*//') log " Installing $name..." if opkg install "$ipk" 2>/dev/null; then installed=$((installed + 1)) else failed=$((failed + 1)) fi done # Fourth pass: install luci-theme-* packages for ipk in "$FEED_DIR"/luci-theme-*.ipk; do [ -f "$ipk" ] || continue total=$((total + 1)) local name=$(basename "$ipk" | sed 's/_[0-9].*//') log " Installing $name..." if opkg install "$ipk" 2>/dev/null; then installed=$((installed + 1)) else failed=$((failed + 1)) fi done log "" log "Installation complete: $installed/$total succeeded, $failed failed" return 0 } # Clean old versions clean_old_versions() { log "Cleaning old package versions..." local removed=0 for name in $(ls "$FEED_DIR"/*.ipk 2>/dev/null | sed 's/_[0-9].*$//' | sort -u); do local basename=$(basename "$name") local versions=$(ls -t "$FEED_DIR/${basename}_"*.ipk 2>/dev/null) local count=$(echo "$versions" | wc -l) if [ "$count" -gt 1 ]; then echo "$versions" | tail -n +2 | while read old; do [ -f "$old" ] || continue verbose "Removing: $(basename "$old")" rm -f "$old" removed=$((removed + 1)) done fi done log "Removed $removed old versions" regenerate_packages } # Start simple HTTP server serve_feed() { local port="${1:-8080}" if command -v uhttpd >/dev/null 2>&1; then log "Feed available at: http://$(uci get network.lan.ipaddr 2>/dev/null || echo ''):$port/secubox-feed/" log "Add to remote opkg: src/gz secubox http://:$port/secubox-feed" # uhttpd is already running for LuCI, feed is at /www/secubox-feed/ else log "uhttpd not available. Feed is at file:///www/secubox-feed/" fi } # Export feed URL export_feed() { local ip=$(uci get network.lan.ipaddr 2>/dev/null || echo "192.168.1.1") local wan_ip=$(curl -s ifconfig.me 2>/dev/null || echo "") echo "Local feed configuration:" echo " src secubox file:///www/secubox-feed" echo "" echo "LAN access (for other OpenWrt devices):" echo " src/gz secubox http://$ip/secubox-feed" echo "" if [ -n "$wan_ip" ]; then echo "WAN access (if port 80 forwarded):" echo " src/gz secubox http://$wan_ip/secubox-feed" fi echo "" echo "Remote hosted feed:" echo " src/gz secubox $REMOTE_FEED_URL" } # ==================== P2P PEER FUNCTIONS ==================== # List mesh peers with active package feeds list_feed_peers() { log "Discovering mesh peers with active feeds..." local result=$(curl -s "http://127.0.0.1:7331/api/factory/packages-sync?refresh=0" 2>/dev/null) if [ -z "$result" ]; then error "Failed to query P2P API. Is secubox-p2p running?" return 1 fi local sources=$(echo "$result" | jsonfilter -e '@.sources[*]' 2>/dev/null | wc -l) if [ "$sources" -eq 0 ]; then log "No peers with active feeds found" return 0 fi printf "%-20s %-15s %-10s %s\n" "NAME" "ADDRESS" "PACKAGES" "STATUS" printf "%-20s %-15s %-10s %s\n" "----" "-------" "--------" "------" local i=0 while [ $i -lt $sources ]; do local node_name=$(echo "$result" | jsonfilter -e "@.sources[$i].node_name" 2>/dev/null) local address=$(echo "$result" | jsonfilter -e "@.sources[$i].address" 2>/dev/null) local pkg_count=$(echo "$result" | jsonfilter -e "@.sources[$i].package_count" 2>/dev/null) local status=$(echo "$result" | jsonfilter -e "@.sources[$i].status" 2>/dev/null) local type=$(echo "$result" | jsonfilter -e "@.sources[$i].type" 2>/dev/null) [ "$type" = "local" ] && address="(local)" printf "%-20s %-15s %-10s %s\n" "$node_name" "$address" "${pkg_count:-0}" "$status" i=$((i + 1)) done local total=$(echo "$result" | jsonfilter -e '@.sync_stats.total_packages' 2>/dev/null) echo "" log "Total packages across all sources: ${total:-0}" } # Search for package across local and all peers search_package() { local pkg_name="$1" [ -z "$pkg_name" ] && { error "Package name required"; return 1; } log "Searching for '$pkg_name' across local and peers..." # First check local if [ -f "$FEED_DIR/Packages" ]; then local local_version=$(awk -v pkg="$pkg_name" ' /^Package:/ { current_pkg = $2 } /^Version:/ { if (current_pkg == pkg) print $2 } ' "$FEED_DIR/Packages") if [ -n "$local_version" ]; then printf "%-15s %-20s %s\n" "LOCAL" "(this node)" "$local_version" fi fi # Search peers local result=$(curl -s "http://127.0.0.1:7331/api/factory/packages-sync" 2>/dev/null) local sources=$(echo "$result" | jsonfilter -e '@.sources[*]' 2>/dev/null | wc -l) local i=0 while [ $i -lt $sources ]; do local type=$(echo "$result" | jsonfilter -e "@.sources[$i].type" 2>/dev/null) [ "$type" = "local" ] && { i=$((i + 1)); continue; } local node_name=$(echo "$result" | jsonfilter -e "@.sources[$i].node_name" 2>/dev/null) local address=$(echo "$result" | jsonfilter -e "@.sources[$i].address" 2>/dev/null) local packages=$(echo "$result" | jsonfilter -e "@.sources[$i].packages" 2>/dev/null) # Search in packages array local pkg_count=$(echo "$packages" | jsonfilter -e '@[*]' 2>/dev/null | wc -l) local j=0 while [ $j -lt $pkg_count ]; do local name=$(echo "$packages" | jsonfilter -e "@[$j].name" 2>/dev/null) if [ "$name" = "$pkg_name" ]; then local version=$(echo "$packages" | jsonfilter -e "@[$j].version" 2>/dev/null) printf "%-15s %-20s %s\n" "PEER" "$address" "$version" break fi j=$((j + 1)) done i=$((i + 1)) done } # Fetch package from specific peer fetch_from_peer() { local pkg_name="$1" local peer_addr="$2" [ -z "$pkg_name" ] && { error "Package name required"; return 1; } [ -z "$peer_addr" ] && { error "Peer address required"; return 1; } log "Fetching '$pkg_name' from $peer_addr..." local result=$(ubus call luci.secubox-p2p fetch_package \ "{\"package\":\"$pkg_name\",\"peer_addr\":\"$peer_addr\"}" 2>/dev/null) if [ -z "$result" ]; then error "Failed to fetch package. Is secubox-p2p running?" return 1 fi local success=$(echo "$result" | jsonfilter -e '@.success' 2>/dev/null) if [ "$success" = "true" ]; then local filename=$(echo "$result" | jsonfilter -e '@.filename' 2>/dev/null) log "Successfully fetched: $filename" log "Run 'secubox-feed update' to refresh local index" return 0 else local err=$(echo "$result" | jsonfilter -e '@.error' 2>/dev/null) error "Failed: ${err:-unknown error}" return 1 fi } # Fetch package from first available peer fetch_from_any() { local pkg_name="$1" [ -z "$pkg_name" ] && { error "Package name required"; return 1; } log "Searching for '$pkg_name' on any peer..." local result=$(ubus call luci.secubox-p2p fetch_package \ "{\"package\":\"$pkg_name\"}" 2>/dev/null) if [ -z "$result" ]; then error "Failed to fetch package. Is secubox-p2p running?" return 1 fi local success=$(echo "$result" | jsonfilter -e '@.success' 2>/dev/null) if [ "$success" = "true" ]; then local source=$(echo "$result" | jsonfilter -e '@.source' 2>/dev/null) local filename=$(echo "$result" | jsonfilter -e '@.filename' 2>/dev/null) log "Successfully fetched from $source: $filename" return 0 else local err=$(echo "$result" | jsonfilter -e '@.error' 2>/dev/null) error "Failed: ${err:-unknown error}" return 1 fi } # Sync package catalogs from all peers sync_peer_catalogs() { log "Syncing package catalogs from all mesh peers..." local result=$(curl -s "http://127.0.0.1:7331/api/factory/packages-sync?refresh=1" 2>/dev/null) if [ -z "$result" ]; then error "Failed to sync. Is secubox-p2p running?" return 1 fi local peers_synced=$(echo "$result" | jsonfilter -e '@.sync_stats.peers_synced' 2>/dev/null) local peers_failed=$(echo "$result" | jsonfilter -e '@.sync_stats.peers_failed' 2>/dev/null) local total=$(echo "$result" | jsonfilter -e '@.sync_stats.total_packages' 2>/dev/null) log "Sync complete:" log " Peers synced: ${peers_synced:-0}" log " Peers failed: ${peers_failed:-0}" log " Total packages: ${total:-0}" } # Auto-install content packages from mesh peers sync_content() { log "Syncing content packages from mesh..." # First sync catalogs sync_peer_catalogs # Get list of content packages from peers local result=$(curl -s "http://127.0.0.1:7331/api/factory/packages-sync" 2>/dev/null) [ -z "$result" ] && return 1 # Extract content packages (secubox-site-* and secubox-streamlit-*) local content_pkgs=$(echo "$result" | jsonfilter -e '@.sources[*].packages[*].name' 2>/dev/null | \ grep -E "^secubox-(site|streamlit)-" | sort -u) if [ -z "$content_pkgs" ]; then log "No content packages found on mesh peers" return 0 fi local installed=0 local failed=0 for pkg in $content_pkgs; do # Check if already installed if opkg status "$pkg" 2>/dev/null | grep -q "Status:.*installed"; then [ "$VERBOSE" = "1" ] && log " Already installed: $pkg" continue fi log " Installing: $pkg" # Find which peer has it and fetch if fetch_from_any "$pkg" >/dev/null 2>&1; then # Install opkg install "$FEED_DIR/${pkg}"*.ipk 2>/dev/null && { installed=$((installed + 1)) log " Installed: $pkg" } || { failed=$((failed + 1)) error " Failed to install: $pkg" } else failed=$((failed + 1)) error " Failed to fetch: $pkg" fi done log "Content sync complete: $installed installed, $failed failed" } # ==================== END P2P FUNCTIONS ==================== # Main main() { local cmd="" VERBOSE=0 QUIET=0 while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; -v|--verbose) VERBOSE=1; shift ;; -q|--quiet) QUIET=1; shift ;; *) if [ -z "$cmd" ]; then cmd="$1" else break fi shift ;; esac done # Ensure feed directory exists mkdir -p "$FEED_DIR" case "$cmd" in update|regenerate) regenerate_packages sync_feed ;; sync) sync_feed ;; fetch) [ -z "$1" ] && { error "URL required"; exit 1; } fetch_ipk "$1" ;; fetch-release|release) [ -z "$1" ] && { error "Package name required"; exit 1; } if [ -n "$GITEA_API" ]; then fetch_gitea_release "$1" elif [ -n "$GITHUB_REPO" ]; then fetch_github_release "$1" else error "Set SECUBOX_GITEA_API or SECUBOX_GITHUB_REPO" exit 1 fi ;; list|ls) list_packages ;; info|show) [ -z "$1" ] && { error "Package name required"; exit 1; } package_info "$1" ;; install) [ -z "$1" ] && { error "Package name required"; exit 1; } install_package "$1" ;; clean) clean_old_versions ;; serve) serve_feed "$1" ;; export|url) export_feed ;; peers) list_feed_peers ;; search) [ -z "$1" ] && { error "Package name required"; exit 1; } search_package "$1" ;; fetch-peer) [ -z "$1" ] && { error "Package name required"; exit 1; } [ -z "$2" ] && { error "Peer address required"; exit 1; } fetch_from_peer "$1" "$2" ;; fetch-any) [ -z "$1" ] && { error "Package name required"; exit 1; } fetch_from_any "$1" ;; sync-peers) sync_peer_catalogs ;; sync-content) sync_content ;; "") usage exit 1 ;; *) error "Unknown command: $cmd" usage exit 1 ;; esac } main "$@"