feat(master-link): Add dynamic join IPK generation for mesh onboarding
Generate a minimal IPK on-the-fly when a client visits the master-link landing page, so the "Download Package" step always works even without a pre-built IPK bundle. The IPK configures the peer via postinst uci commands (avoiding file conflicts with secubox-master-link), and can be installed directly via opkg install URL from SSH. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cd4e991761
commit
857622ff56
@ -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() {
|
ml_ipk_serve() {
|
||||||
local token="$1"
|
local token="$1"
|
||||||
|
|
||||||
@ -390,37 +472,58 @@ ml_ipk_serve() {
|
|||||||
if [ "$valid" != "true" ]; then
|
if [ "$valid" != "true" ]; then
|
||||||
echo "Status: 403 Forbidden"
|
echo "Status: 403 Forbidden"
|
||||||
echo "Content-Type: application/json"
|
echo "Content-Type: application/json"
|
||||||
|
echo "Access-Control-Allow-Origin: *"
|
||||||
echo ""
|
echo ""
|
||||||
echo "$validation"
|
echo "$validation"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Find IPK file
|
# Try pre-built IPK first
|
||||||
local ipk_path=$(uci -q get master-link.main.ipk_path)
|
local ipk_path=$(uci -q get master-link.main.ipk_path)
|
||||||
[ -z "$ipk_path" ] && ipk_path="/www/secubox-feed/secubox-master-link_*.ipk"
|
[ -z "$ipk_path" ] && ipk_path="/www/secubox-feed/secubox-master-link_*.ipk"
|
||||||
|
|
||||||
# Resolve glob
|
|
||||||
local ipk_file=""
|
local ipk_file=""
|
||||||
for f in $ipk_path; do
|
for f in $ipk_path; do
|
||||||
[ -f "$f" ] && ipk_file="$f"
|
[ -f "$f" ] && ipk_file="$f"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Fallback: generate minimal join IPK on-the-fly
|
||||||
|
local generated=0
|
||||||
if [ -z "$ipk_file" ]; then
|
if [ -z "$ipk_file" ]; then
|
||||||
echo "Status: 404 Not Found"
|
local master_ip=$(_ml_detect_master_ip)
|
||||||
echo "Content-Type: application/json"
|
local master_depth=$(uci -q get master-link.main.depth)
|
||||||
echo ""
|
[ -z "$master_depth" ] && master_depth=0
|
||||||
echo '{"error":"ipk_not_found"}'
|
local master_hostname=$(uci -q get system.@system[0].hostname 2>/dev/null)
|
||||||
return 1
|
[ -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
|
fi
|
||||||
|
|
||||||
local filename=$(basename "$ipk_file")
|
local filename="secubox-mesh-join.ipk"
|
||||||
|
[ "$generated" = "0" ] && filename=$(basename "$ipk_file")
|
||||||
local filesize=$(wc -c < "$ipk_file")
|
local filesize=$(wc -c < "$ipk_file")
|
||||||
|
|
||||||
echo "Content-Type: application/octet-stream"
|
echo "Content-Type: application/octet-stream"
|
||||||
echo "Content-Disposition: attachment; filename=\"$filename\""
|
echo "Content-Disposition: attachment; filename=\"$filename\""
|
||||||
echo "Content-Length: $filesize"
|
echo "Content-Length: $filesize"
|
||||||
|
echo "Access-Control-Allow-Origin: *"
|
||||||
echo ""
|
echo ""
|
||||||
cat "$ipk_file"
|
cat "$ipk_file"
|
||||||
|
|
||||||
|
# Clean up generated IPK
|
||||||
|
[ "$generated" = "1" ] && rm -f "$ipk_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Return IPK metadata
|
# Return IPK metadata
|
||||||
@ -433,23 +536,31 @@ ml_ipk_bundle_info() {
|
|||||||
[ -f "$f" ] && ipk_file="$f"
|
[ -f "$f" ] && ipk_file="$f"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "$ipk_file" ]; then
|
if [ -n "$ipk_file" ]; then
|
||||||
echo '{"available":false}'
|
local filename=$(basename "$ipk_file")
|
||||||
return 1
|
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
|
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)
|
ipk-info)
|
||||||
ml_ipk_bundle_info
|
ml_ipk_bundle_info
|
||||||
;;
|
;;
|
||||||
|
ipk-generate)
|
||||||
|
ml_ipk_generate "$2" "$3" "$4" "$5"
|
||||||
|
;;
|
||||||
init)
|
init)
|
||||||
ml_init
|
ml_init
|
||||||
echo "Master-link initialized"
|
echo "Master-link initialized"
|
||||||
|
|||||||
@ -1,25 +1,20 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Master-Link API - Serve SecuBox IPK bundle
|
# Master-Link API - Serve SecuBox IPK (pre-built or generated on-the-fly)
|
||||||
# POST /api/master-link/ipk
|
# 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
|
# Auth: Token-validated
|
||||||
|
|
||||||
# NOTE: Headers are sent by ml_ipk_serve, not here
|
# Handle CORS preflight
|
||||||
# Handle CORS preflight first
|
|
||||||
if [ "$REQUEST_METHOD" = "OPTIONS" ]; then
|
if [ "$REQUEST_METHOD" = "OPTIONS" ]; then
|
||||||
echo "Content-Type: text/plain"
|
echo "Content-Type: text/plain"
|
||||||
echo "Access-Control-Allow-Origin: *"
|
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 "Access-Control-Allow-Headers: Content-Type"
|
||||||
echo ""
|
echo ""
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$REQUEST_METHOD" = "GET" ]; then
|
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
|
# Load library
|
||||||
. /usr/lib/secubox/master-link.sh >/dev/null 2>&1
|
. /usr/lib/secubox/master-link.sh >/dev/null 2>&1
|
||||||
|
|
||||||
@ -30,10 +25,14 @@ if [ "$REQUEST_METHOD" = "GET" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
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
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ml_ipk_serve handles all headers (Content-Type, Content-Length, etc.)
|
||||||
ml_ipk_serve "$token"
|
ml_ipk_serve "$token"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@ -46,12 +45,10 @@ if [ "$REQUEST_METHOD" != "POST" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Load library
|
# POST: token in JSON body
|
||||||
. /usr/lib/secubox/master-link.sh >/dev/null 2>&1
|
. /usr/lib/secubox/master-link.sh >/dev/null 2>&1
|
||||||
|
|
||||||
# Read POST body
|
|
||||||
read -r input
|
read -r input
|
||||||
|
|
||||||
token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)
|
token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
if [ -z "$token" ]; then
|
||||||
|
|||||||
@ -56,6 +56,11 @@ else
|
|||||||
[ -z "$depth" ] && depth=0
|
[ -z "$depth" ] && depth=0
|
||||||
ipk_info=$(ml_ipk_bundle_info 2>/dev/null)
|
ipk_info=$(ml_ipk_bundle_info 2>/dev/null)
|
||||||
ipk_available=$(echo "$ipk_info" | jsonfilter -e '@.available' 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
|
cat <<-EOF
|
||||||
{
|
{
|
||||||
@ -63,7 +68,9 @@ else
|
|||||||
"fingerprint": "$fp",
|
"fingerprint": "$fp",
|
||||||
"hostname": "$hostname",
|
"hostname": "$hostname",
|
||||||
"depth": $depth,
|
"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
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -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; }
|
.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; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -108,9 +122,24 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="action-download" class="card hidden">
|
<div id="action-download" class="card hidden">
|
||||||
<div class="card-title">Download Package</div>
|
<div class="card-title">Join Package</div>
|
||||||
<div id="ipk-info"></div>
|
<div id="ipk-info"></div>
|
||||||
<button class="primary" id="btn-download" onclick="downloadIPK()">Download SecuBox IPK</button>
|
<div id="ipk-contents" class="ipk-contents hidden">
|
||||||
|
<div class="label">This package configures your device as:</div>
|
||||||
|
<div class="field">role = <span>peer</span></div>
|
||||||
|
<div class="field">upstream = <span id="ipk-upstream">-</span></div>
|
||||||
|
<div class="field">depth = <span id="ipk-depth">-</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="ipk-cmd" class="hidden" style="margin-bottom:0.75rem;">
|
||||||
|
<div style="color:var(--muted);font-size:0.75rem;margin-bottom:0.25rem;">Install directly via SSH:</div>
|
||||||
|
<div class="cmd-box" id="opkg-cmd" onclick="copyCmd(this)">
|
||||||
|
<span class="copy-hint">click to copy</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="primary" id="btn-download" onclick="downloadIPK()">Download IPK</button>
|
||||||
|
<button class="secondary" id="btn-skip" onclick="showJoinStep()">Skip</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="action-join" class="card hidden">
|
<div id="action-join" class="card hidden">
|
||||||
@ -162,21 +191,29 @@
|
|||||||
'<div class="info-row"><span class="info-label">Fingerprint</span><span class="info-value fingerprint">' + (masterInfo.fingerprint || '-') + '</span></div>' +
|
'<div class="info-row"><span class="info-label">Fingerprint</span><span class="info-value fingerprint">' + (masterInfo.fingerprint || '-') + '</span></div>' +
|
||||||
'<div class="info-row"><span class="info-label">Role</span><span class="info-value">' + (masterInfo.role || '-') + '</span></div>' +
|
'<div class="info-row"><span class="info-label">Role</span><span class="info-value">' + (masterInfo.role || '-') + '</span></div>' +
|
||||||
'<div class="info-row"><span class="info-label">Depth</span><span class="info-value">' + (masterInfo.depth || 0) + '</span></div>' +
|
'<div class="info-row"><span class="info-label">Depth</span><span class="info-value">' + (masterInfo.depth || 0) + '</span></div>' +
|
||||||
(masterInfo.ipk_available ? '<div class="info-row"><span class="info-label">IPK</span><span class="status-badge status-ok">Available</span></div>' : '');
|
'<div class="info-row"><span class="info-label">Join Package</span><span class="status-badge status-ok">Ready</span></div>';
|
||||||
|
|
||||||
// Move to step 2
|
// Move to step 2
|
||||||
setStep(1);
|
setStep(1);
|
||||||
document.getElementById('action-download').classList.remove('hidden');
|
document.getElementById('action-download').classList.remove('hidden');
|
||||||
|
|
||||||
if (masterInfo.ipk_available) {
|
// Build IPK download URL and opkg command
|
||||||
document.getElementById('ipk-info').innerHTML =
|
var ipkUrl = 'http://' + location.host + '/api/master-link/ipk?token=' + encodeURIComponent(token);
|
||||||
'<p style="color:var(--muted);font-size:0.8rem;margin-bottom:0.75rem;">The SecuBox package is ready for download. Install it on your device with: <code style="background:var(--bg);padding:0.15rem 0.4rem;border-radius:0.2rem;">opkg install secubox*.ipk</code></p>';
|
var opkgCmd = 'opkg install "' + ipkUrl + '"';
|
||||||
} else {
|
var peerDepth = (masterInfo.depth || 0) + 1;
|
||||||
document.getElementById('ipk-info').innerHTML =
|
|
||||||
'<p style="color:var(--muted);font-size:0.8rem;margin-bottom:0.75rem;">No IPK bundle available on this master. You can skip to joining if SecuBox is already installed.</p>';
|
document.getElementById('ipk-info').innerHTML =
|
||||||
document.getElementById('btn-download').textContent = 'Skip to Join';
|
'<p style="color:var(--muted);font-size:0.8rem;margin-bottom:0.75rem;">' +
|
||||||
document.getElementById('btn-download').onclick = function() { showJoinStep(); };
|
'Install this package to join <strong>' + (masterInfo.hostname || 'master') + '</strong> as a mesh peer.</p>';
|
||||||
}
|
|
||||||
|
// Show IPK contents preview
|
||||||
|
document.getElementById('ipk-upstream').textContent = location.hostname || '-';
|
||||||
|
document.getElementById('ipk-depth').textContent = peerDepth;
|
||||||
|
document.getElementById('ipk-contents').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Show opkg install command
|
||||||
|
document.getElementById('opkg-cmd').insertAdjacentHTML('afterbegin', opkgCmd);
|
||||||
|
document.getElementById('ipk-cmd').classList.remove('hidden');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('master-info').innerHTML =
|
document.getElementById('master-info').innerHTML =
|
||||||
'<div class="error">Could not reach master node: ' + e.message + '</div>';
|
'<div class="error">Could not reach master node: ' + e.message + '</div>';
|
||||||
@ -285,6 +322,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyCmd(el) {
|
||||||
|
var text = el.textContent.replace('click to copy', '').replace('copied!', '').trim();
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
var hint = el.querySelector('.copy-hint');
|
||||||
|
if (hint) { hint.textContent = 'copied!'; setTimeout(function() { hint.textContent = 'click to copy'; }, 2000); }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function generateFingerprint() {
|
async function generateFingerprint() {
|
||||||
// Generate a browser-side fingerprint (temporary; real one comes from installed SecuBox)
|
// Generate a browser-side fingerprint (temporary; real one comes from installed SecuBox)
|
||||||
var data = navigator.userAgent + Date.now() + Math.random();
|
var data = navigator.userAgent + Date.now() + Math.random();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user