diff --git a/package/secubox/secubox-master-link/files/usr/lib/secubox/master-link.sh b/package/secubox/secubox-master-link/files/usr/lib/secubox/master-link.sh index 311f21d6..7dd53abb 100644 --- a/package/secubox/secubox-master-link/files/usr/lib/secubox/master-link.sh +++ b/package/secubox/secubox-master-link/files/usr/lib/secubox/master-link.sh @@ -376,10 +376,92 @@ ml_join_reject() { } # ============================================================================ -# IPK Serving +# IPK Generation & Serving # ============================================================================ -# Validate token and serve IPK file +# Generate a minimal join IPK on-the-fly +# OpenWrt IPK format: tar.gz containing debian-binary, control.tar.gz, data.tar.gz +# Args: $1=master_ip $2=master_depth $3=master_hostname $4=token_prefix +# Output: path to generated IPK file on stdout +ml_ipk_generate() { + local master_ip="$1" + local master_depth="${2:-0}" + local master_hostname="${3:-secubox}" + local token_prefix="${4:-0000}" + local peer_depth=$((master_depth + 1)) + local version="1.0.0-${token_prefix}" + + local work="/tmp/ml-ipk-$$" + local ipk_file="/tmp/secubox-mesh-join-$$.ipk" + + rm -rf "$work" + mkdir -p "$work/ipk" "$work/control" "$work/data" + + # --- debian-binary --- + printf '2.0\n' > "$work/ipk/debian-binary" + + # --- control file --- + cat > "$work/control/control" <<-CTRL + Package: secubox-mesh-join + Version: ${version} + Architecture: all + Installed-Size: 1 + Description: SecuBox mesh join - peer of ${master_hostname} (${master_ip}) + Maintainer: SecuBox + Section: admin + CTRL + + # --- postinst script --- + # Configure master-link via uci instead of shipping config file + # (avoids file conflict with secubox-master-link package) + cat > "$work/control/postinst" <<-POSTINST + #!/bin/sh + [ -n "\${IPKG_INSTROOT}" ] || { + uci -q set master-link.main=master-link + uci -q set master-link.main.enabled='1' + uci -q set master-link.main.role='peer' + uci -q set master-link.main.upstream='${master_ip}' + uci -q set master-link.main.depth='${peer_depth}' + uci -q set master-link.main.max_depth='3' + uci -q set master-link.main.token_ttl='3600' + uci -q set master-link.main.auto_approve='0' + uci commit master-link + /etc/init.d/master-link enable 2>/dev/null + /etc/init.d/master-link start 2>/dev/null + } + exit 0 + POSTINST + chmod 755 "$work/control/postinst" + + # --- data: empty (config is applied by postinst via uci) --- + + # --- Build inner tar.gz archives --- + tar czf "$work/ipk/control.tar.gz" -C "$work/control" . 2>/dev/null + tar czf "$work/ipk/data.tar.gz" -C "$work/data" . 2>/dev/null + + # --- Assemble IPK (outer tar.gz) --- + tar czf "$ipk_file" -C "$work/ipk" \ + debian-binary control.tar.gz data.tar.gz 2>/dev/null + + rm -rf "$work" + echo "$ipk_file" +} + +# Detect master IP from CGI environment or UCI +_ml_detect_master_ip() { + # Try HTTP_HOST (CGI environment, set by uhttpd) + local ip=$(echo "${HTTP_HOST:-}" | cut -d: -f1) + [ -n "$ip" ] && { echo "$ip"; return; } + + # Try UCI network config + ip=$(uci -q get network.lan.ipaddr) + [ -n "$ip" ] && { echo "$ip"; return; } + + # Fallback: parse ip addr + ip addr show br-lan 2>/dev/null | grep -o 'inet [0-9.]*' | head -1 | cut -d' ' -f2 +} + +# Validate token and serve IPK file (pre-built or generated) ml_ipk_serve() { local token="$1" @@ -390,37 +472,58 @@ ml_ipk_serve() { if [ "$valid" != "true" ]; then echo "Status: 403 Forbidden" echo "Content-Type: application/json" + echo "Access-Control-Allow-Origin: *" echo "" echo "$validation" return 1 fi - # Find IPK file + # Try pre-built IPK first local ipk_path=$(uci -q get master-link.main.ipk_path) [ -z "$ipk_path" ] && ipk_path="/www/secubox-feed/secubox-master-link_*.ipk" - # Resolve glob local ipk_file="" for f in $ipk_path; do [ -f "$f" ] && ipk_file="$f" done + # Fallback: generate minimal join IPK on-the-fly + local generated=0 if [ -z "$ipk_file" ]; then - echo "Status: 404 Not Found" - echo "Content-Type: application/json" - echo "" - echo '{"error":"ipk_not_found"}' - return 1 + local master_ip=$(_ml_detect_master_ip) + local master_depth=$(uci -q get master-link.main.depth) + [ -z "$master_depth" ] && master_depth=0 + local master_hostname=$(uci -q get system.@system[0].hostname 2>/dev/null) + [ -z "$master_hostname" ] && master_hostname="secubox" + local token_hash=$(echo "$token" | sha256sum | cut -d' ' -f1) + local token_prefix=$(echo "$token_hash" | cut -c1-8) + + ipk_file=$(ml_ipk_generate "$master_ip" "$master_depth" "$master_hostname" "$token_prefix") + generated=1 + + if [ -z "$ipk_file" ] || [ ! -f "$ipk_file" ]; then + echo "Status: 500 Internal Server Error" + echo "Content-Type: application/json" + echo "Access-Control-Allow-Origin: *" + echo "" + echo '{"error":"ipk_generation_failed"}' + return 1 + fi fi - local filename=$(basename "$ipk_file") + local filename="secubox-mesh-join.ipk" + [ "$generated" = "0" ] && filename=$(basename "$ipk_file") local filesize=$(wc -c < "$ipk_file") echo "Content-Type: application/octet-stream" echo "Content-Disposition: attachment; filename=\"$filename\"" echo "Content-Length: $filesize" + echo "Access-Control-Allow-Origin: *" echo "" cat "$ipk_file" + + # Clean up generated IPK + [ "$generated" = "1" ] && rm -f "$ipk_file" } # Return IPK metadata @@ -433,23 +536,31 @@ ml_ipk_bundle_info() { [ -f "$f" ] && ipk_file="$f" done - if [ -z "$ipk_file" ]; then - echo '{"available":false}' - return 1 + if [ -n "$ipk_file" ]; then + local filename=$(basename "$ipk_file") + local filesize=$(wc -c < "$ipk_file") + local sha256=$(sha256sum "$ipk_file" | cut -d' ' -f1) + + cat <<-EOF + { + "available": true, + "type": "prebuilt", + "filename": "$filename", + "size": $filesize, + "sha256": "$sha256" + } + EOF + else + # Dynamic generation always available + cat <<-EOF + { + "available": true, + "type": "dynamic", + "filename": "secubox-mesh-join.ipk", + "description": "Minimal join package (generated on-the-fly)" + } + EOF fi - - local filename=$(basename "$ipk_file") - local filesize=$(wc -c < "$ipk_file") - local sha256=$(sha256sum "$ipk_file" | cut -d' ' -f1) - - cat <<-EOF - { - "available": true, - "filename": "$filename", - "size": $filesize, - "sha256": "$sha256" - } - EOF } # ============================================================================ @@ -784,6 +895,9 @@ case "${1:-}" in ipk-info) ml_ipk_bundle_info ;; + ipk-generate) + ml_ipk_generate "$2" "$3" "$4" "$5" + ;; init) ml_init echo "Master-link initialized" diff --git a/package/secubox/secubox-master-link/files/www/api/master-link/ipk b/package/secubox/secubox-master-link/files/www/api/master-link/ipk index c25d76d5..f240c991 100644 --- a/package/secubox/secubox-master-link/files/www/api/master-link/ipk +++ b/package/secubox/secubox-master-link/files/www/api/master-link/ipk @@ -1,25 +1,20 @@ #!/bin/sh -# Master-Link API - Serve SecuBox IPK bundle -# POST /api/master-link/ipk +# Master-Link API - Serve SecuBox IPK (pre-built or generated on-the-fly) +# GET /api/master-link/ipk?token=TOKEN — direct download / opkg install +# POST /api/master-link/ipk — download with token in JSON body # Auth: Token-validated -# NOTE: Headers are sent by ml_ipk_serve, not here -# Handle CORS preflight first +# Handle CORS preflight if [ "$REQUEST_METHOD" = "OPTIONS" ]; then echo "Content-Type: text/plain" echo "Access-Control-Allow-Origin: *" - echo "Access-Control-Allow-Methods: POST, OPTIONS" + echo "Access-Control-Allow-Methods: GET, POST, OPTIONS" echo "Access-Control-Allow-Headers: Content-Type" echo "" exit 0 fi if [ "$REQUEST_METHOD" = "GET" ]; then - # GET with query string token - for direct download - echo "Content-Type: application/json" - echo "Access-Control-Allow-Origin: *" - echo "" - # Load library . /usr/lib/secubox/master-link.sh >/dev/null 2>&1 @@ -30,10 +25,14 @@ if [ "$REQUEST_METHOD" = "GET" ]; then fi if [ -z "$token" ]; then - echo '{"error":"missing_token","hint":"POST with {\"token\":\"...\"} or GET with ?token=..."}' + echo "Content-Type: application/json" + echo "Access-Control-Allow-Origin: *" + echo "" + echo '{"error":"missing_token","hint":"GET with ?token=TOKEN"}' exit 0 fi + # ml_ipk_serve handles all headers (Content-Type, Content-Length, etc.) ml_ipk_serve "$token" exit 0 fi @@ -46,12 +45,10 @@ if [ "$REQUEST_METHOD" != "POST" ]; then exit 0 fi -# Load library +# POST: token in JSON body . /usr/lib/secubox/master-link.sh >/dev/null 2>&1 -# Read POST body read -r input - token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null) if [ -z "$token" ]; then diff --git a/package/secubox/secubox-master-link/files/www/api/master-link/status b/package/secubox/secubox-master-link/files/www/api/master-link/status index 8fa7a066..6cf31c0a 100644 --- a/package/secubox/secubox-master-link/files/www/api/master-link/status +++ b/package/secubox/secubox-master-link/files/www/api/master-link/status @@ -56,6 +56,11 @@ else [ -z "$depth" ] && depth=0 ipk_info=$(ml_ipk_bundle_info 2>/dev/null) ipk_available=$(echo "$ipk_info" | jsonfilter -e '@.available' 2>/dev/null) + ipk_type=$(echo "$ipk_info" | jsonfilter -e '@.type' 2>/dev/null) + + # Detect master IP for IPK URL + master_ip=$(echo "${HTTP_HOST:-}" | cut -d: -f1) + [ -z "$master_ip" ] && master_ip=$(uci -q get network.lan.ipaddr) cat <<-EOF { @@ -63,7 +68,9 @@ else "fingerprint": "$fp", "hostname": "$hostname", "depth": $depth, - "ipk_available": ${ipk_available:-false} + "ipk_available": ${ipk_available:-false}, + "ipk_type": "${ipk_type:-unknown}", + "ipk_url_base": "http://${master_ip}:7331/api/master-link/ipk" } EOF fi diff --git a/package/secubox/secubox-master-link/files/www/master-link/index.html b/package/secubox/secubox-master-link/files/www/master-link/index.html index 77d9d4a5..013de789 100644 --- a/package/secubox/secubox-master-link/files/www/master-link/index.html +++ b/package/secubox/secubox-master-link/files/www/master-link/index.html @@ -61,6 +61,20 @@ .success-msg { color: var(--success); font-size: 0.85rem; padding: 0.75rem; background: rgba(34, 197, 94, 0.1); border-radius: 0.375rem; margin-top: 0.5rem; } .hidden { display: none; } + + .ipk-contents { background: var(--bg); padding: 0.6rem 0.8rem; border-radius: 0.375rem; margin-bottom: 0.75rem; font-size: 0.75rem; } + .ipk-contents .label { color: var(--muted); margin-bottom: 0.25rem; } + .ipk-contents .field { font-family: ui-monospace, monospace; padding: 0.1rem 0; } + .ipk-contents .field span { color: var(--accent); } + + .cmd-box { background: var(--bg); padding: 0.5rem 0.8rem; border-radius: 0.375rem; font-family: ui-monospace, monospace; font-size: 0.75rem; word-break: break-all; cursor: pointer; position: relative; border: 1px solid var(--border); transition: border-color 0.2s; } + .cmd-box:hover { border-color: var(--accent); } + .cmd-box .copy-hint { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); color: var(--muted); font-size: 0.65rem; font-family: system-ui, sans-serif; } + + .btn-row { display: flex; gap: 0.5rem; } + .btn-row button { flex: 1; } + button.secondary { background: var(--border); color: var(--text); } + button.secondary:hover { background: #475569; }
@@ -108,9 +122,24 @@