secubox-openwrt/package/secubox/secubox-app-bonus/root/usr/sbin/secubox-feed
CyberMind-FR 304ac7b9a1 feat: P2P App Store, Remote Access & Mesh Media packages
P2P App Store Emancipation:
- secubox-p2p: Package distribution via mesh peers (CGI API, RPCD, CLI)
- packages.js: LuCI view with LOCAL/PEER badges, fetch/install actions
- devstatus.js: Dev Status widget with Gitea commits, v1.0 progress tracking
- secubox-feed: sync-content command for auto-installing content packages
- ACL fix for P2P feed RPCD methods

Remote Access:
- secubox-app-rustdesk: Native hbbs/hbbr relay server from GitHub releases
- secubox-app-guacamole: LXC Debian container with guacd + Tomcat (partial)

Content Distribution:
- secubox-content-pkg: Auto-package Metablogizer/Streamlit as IPKs
- Auto-publish hooks in metablogizerctl and streamlitctl

Mesh Media:
- secubox-app-ksmbd: In-kernel SMB3 server with ksmbdctl CLI
- Pre-configured shares for Jellyfin, Lyrion, Backup

UI Consistency:
- client-guardian: Ported to sh-page-header chip layout
- auth-guardian: Ported to sh-page-header chip layout

Fixes:
- services.js: RPC expect unwrapping bug fix
- metablogizer: Chunked upload for uhttpd 64KB limit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:33:53 +01:00

712 lines
21 KiB
Bash

#!/bin/sh
# SecuBox Feed Manager
# Manages local and remote package feeds for SecuBox appstore
# Copyright 2026 CyberMind <contact@cybermind.fr>
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 <command> [options]
Commands:
update Update local feed from installed IPKs
sync Sync feed to /var/opkg-lists for opkg
fetch <url> Fetch IPK from URL and add to local feed
fetch-release <pkg> Fetch latest release from GitHub/Gitea
list List packages in local feed
info <pkg> Show package info
install <pkg> 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 <pkg> Search for package across local and all peers
fetch-peer <pkg> <peer> Fetch package from specific peer
fetch-any <pkg> 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 '<router-ip>'):$port/secubox-feed/"
log "Add to remote opkg: src/gz secubox http://<router-ip>:$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 "$@"