diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 8103d959..6e476dca 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -1040,3 +1040,13 @@ _Last updated: 2026-02-10_ - Live indicator with timestamp display. - Efficient updateValue/updateBar/updateList methods. - Tag: v0.19.16 + +34. **Cloning Station LuCI Dashboard (2026-02-11)** + - Created `luci-app-cloner` package for station cloning management. + - Dashboard shows: device type, TFTP status, image info, tokens, clones. + - Quick actions: Build Image, Start/Stop TFTP, New Token, Auto-Approve Token. + - Clone images table with TFTP-ready status. + - Token management with create/delete functionality. + - U-Boot flash commands display when TFTP is running. + - RPCD handler with 10 methods for status, images, tokens, clones. + - Tag: v0.19.20 diff --git a/.claude/WIP.md b/.claude/WIP.md index 83c9f729..91b2bfd0 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -120,6 +120,15 @@ _Last updated: 2026-02-09 (early morning)_ - Added `secubox clone` and `secubox master-link` CLI command groups - Full workflow: build image on master → TFTP serve → flash target → auto-join mesh +- **Cloning Station LuCI Dashboard** — DONE (2026-02-11) + - Created `luci-app-cloner` package with KISS-style dashboard + - Status cards: device type, TFTP status, token count, clone count + - Quick actions: Build Image, Start/Stop TFTP, New/Auto-Approve Token + - Clone images table with size and TFTP-ready indicator + - Token management with delete functionality + - U-Boot flash commands display when TFTP active + - RPCD handler: 10 methods (status, list_images, list_tokens, list_clones, etc.) + - **HAProxy "End of Internet" Default Page** — DONE (2026-02-07) - Cyberpunk fallback page for unknown/unmatched domains - Matrix rain animation, glitch text, ASCII art SecuBox logo diff --git a/package/secubox/luci-app-cloner/Makefile b/package/secubox/luci-app-cloner/Makefile new file mode 100644 index 00000000..cedbb813 --- /dev/null +++ b/package/secubox/luci-app-cloner/Makefile @@ -0,0 +1,32 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI Cloning Station Dashboard +LUCI_DESCRIPTION:=SecuBox cloning station for building and deploying clone images +LUCI_DEPENDS:=+luci-base +rpcd +secubox-core +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-cloner +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=SecuBox Team + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/$(PKG_NAME)/conffiles +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-cloner.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-cloner.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.cloner $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/cloner + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/cloner/*.js $(1)/www/luci-static/resources/view/cloner/ +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js b/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js new file mode 100644 index 00000000..2da36d81 --- /dev/null +++ b/package/secubox/luci-app-cloner/htdocs/luci-static/resources/view/cloner/overview.js @@ -0,0 +1,331 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +var callGetStatus = rpc.declare({ + object: 'luci.cloner', + method: 'status', + expect: { } +}); + +var callListImages = rpc.declare({ + object: 'luci.cloner', + method: 'list_images', + expect: { images: [] } +}); + +var callListTokens = rpc.declare({ + object: 'luci.cloner', + method: 'list_tokens', + expect: { tokens: [] } +}); + +var callListClones = rpc.declare({ + object: 'luci.cloner', + method: 'list_clones', + expect: { clones: [] } +}); + +var callGenerateToken = rpc.declare({ + object: 'luci.cloner', + method: 'generate_token', + params: ['auto_approve'] +}); + +var callBuildImage = rpc.declare({ + object: 'luci.cloner', + method: 'build_image' +}); + +var callTftpStart = rpc.declare({ + object: 'luci.cloner', + method: 'tftp_start' +}); + +var callTftpStop = rpc.declare({ + object: 'luci.cloner', + method: 'tftp_stop' +}); + +var callDeleteToken = rpc.declare({ + object: 'luci.cloner', + method: 'delete_token', + params: ['token'] +}); + +return view.extend({ + load: function() { + return Promise.all([ + callGetStatus(), + callListImages(), + callListTokens(), + callListClones() + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var images = data[1].images || []; + var tokens = data[2].tokens || []; + var clones = data[3].clones || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + E('h2', {}, 'Cloning Station'), + E('div', { 'class': 'cbi-map-descr' }, 'Build and deploy SecuBox clone images to new devices'), + + // Status Cards + E('div', { 'style': 'display:flex;gap:20px;margin:20px 0;flex-wrap:wrap;' }, [ + E('div', { 'style': 'padding:15px;background:#3b82f622;border-radius:8px;min-width:120px;' }, [ + E('div', { 'style': 'font-size:12px;color:#888;' }, 'Device'), + E('strong', { 'style': 'font-size:16px;color:#3b82f6;' }, status.device_type || 'unknown') + ]), + E('div', { 'style': 'padding:15px;border-radius:8px;min-width:120px;', 'class': status.tftp_running ? 'tftp-on' : 'tftp-off' }, [ + E('div', { 'style': 'font-size:12px;color:#888;' }, 'TFTP'), + E('strong', { 'style': 'font-size:16px;', 'id': 'tftp-status' }, + status.tftp_running ? 'Running' : 'Stopped') + ]), + E('div', { 'style': 'padding:15px;background:#8b5cf622;border-radius:8px;min-width:120px;' }, [ + E('div', { 'style': 'font-size:12px;color:#888;' }, 'Tokens'), + E('strong', { 'style': 'font-size:24px;color:#8b5cf6;', 'id': 'token-count' }, + String(tokens.length)) + ]), + E('div', { 'style': 'padding:15px;background:#22c55e22;border-radius:8px;min-width:120px;' }, [ + E('div', { 'style': 'font-size:12px;color:#888;' }, 'Clones'), + E('strong', { 'style': 'font-size:24px;color:#22c55e;', 'id': 'clone-count' }, + String(status.clone_count || 0)) + ]) + ]), + + // Quick Actions + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Quick Actions'), + E('div', { 'style': 'display:flex;gap:10px;flex-wrap:wrap;' }, [ + this.createActionButton('Build Image', 'cbi-button-action', L.bind(this.handleBuild, this)), + this.createActionButton(status.tftp_running ? 'Stop TFTP' : 'Start TFTP', + status.tftp_running ? 'cbi-button-negative' : 'cbi-button-positive', + L.bind(this.handleTftp, this, !status.tftp_running)), + this.createActionButton('New Token', 'cbi-button-action', L.bind(this.handleNewToken, this)), + this.createActionButton('Auto-Approve Token', 'cbi-button-save', L.bind(this.handleAutoToken, this)) + ]) + ]), + + // Clone Images + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Clone Images'), + E('table', { 'class': 'table', 'id': 'images-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Name'), + E('th', { 'class': 'th' }, 'Size'), + E('th', { 'class': 'th' }, 'TFTP Ready'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ].concat(images.length > 0 ? images.map(L.bind(this.renderImageRow, this)) : + [E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 4, 'style': 'text-align:center;' }, + 'No images available. Click "Build Image" to create one.')])] + )) + ]), + + // Tokens + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Clone Tokens'), + E('table', { 'class': 'table', 'id': 'tokens-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Token'), + E('th', { 'class': 'th' }, 'Created'), + E('th', { 'class': 'th' }, 'Type'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ].concat(tokens.length > 0 ? tokens.map(L.bind(this.renderTokenRow, this)) : + [E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 4, 'style': 'text-align:center;' }, + 'No tokens. Click "New Token" to generate one.')])] + )) + ]), + + // TFTP Instructions + status.tftp_running ? E('div', { 'class': 'cbi-section', 'style': 'background:#22c55e11;padding:15px;border-radius:8px;border-left:4px solid #22c55e;' }, [ + E('h3', { 'style': 'margin-top:0;' }, 'U-Boot Flash Commands'), + E('p', {}, 'Run these commands in U-Boot (Marvell>> prompt) on the target device:'), + E('pre', { 'style': 'background:#000;color:#0f0;padding:10px;border-radius:4px;overflow-x:auto;' }, + 'setenv serverip ' + status.lan_ip + '\n' + + 'setenv ipaddr 192.168.255.100\n' + + 'dhcp\n' + + 'tftpboot 0x6000000 secubox-clone.img\n' + + 'mmc dev 1\n' + + 'mmc write 0x6000000 0 ${filesize}\n' + + 'reset' + ) + ]) : null, + + // Cloned Devices + E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Cloned Devices'), + E('table', { 'class': 'table', 'id': 'clones-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Device'), + E('th', { 'class': 'th' }, 'Status') + ]) + ].concat(clones.length > 0 ? clones.map(L.bind(this.renderCloneRow, this)) : + [E('tr', { 'class': 'tr' }, [E('td', { 'class': 'td', 'colspan': 2, 'style': 'text-align:center;' }, + 'No clones yet.')])] + )) + ]) + ].filter(Boolean)); + + // Add dynamic styles + var style = E('style', {}, [ + '.tftp-on { background: #22c55e22; }', + '.tftp-on strong { color: #22c55e; }', + '.tftp-off { background: #64748b22; }', + '.tftp-off strong { color: #64748b; }' + ].join('\n')); + view.insertBefore(style, view.firstChild); + + poll.add(L.bind(this.refresh, this), 10); + return view; + }, + + createActionButton: function(label, cls, handler) { + var btn = E('button', { 'class': 'cbi-button ' + cls, 'style': 'padding:8px 16px;' }, label); + btn.addEventListener('click', handler); + return btn; + }, + + renderImageRow: function(img) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'style': 'font-family:monospace;' }, img.name), + E('td', { 'class': 'td' }, img.size), + E('td', { 'class': 'td' }, img.tftp_ready ? 'Yes' : 'No'), + E('td', { 'class': 'td' }, '-') + ]); + }, + + renderTokenRow: function(tok) { + var typeLabel = tok.auto_approve ? 'Auto-Approve' : 'Manual'; + var usedLabel = tok.used ? ' (used)' : ''; + var style = tok.used ? 'opacity:0.5;' : ''; + + var deleteBtn = E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'padding:2px 8px;font-size:12px;', + 'data-token': tok.token + }, 'Delete'); + deleteBtn.addEventListener('click', L.bind(this.handleDeleteToken, this)); + + return E('tr', { 'class': 'tr', 'style': style }, [ + E('td', { 'class': 'td', 'style': 'font-family:monospace;' }, tok.token_short), + E('td', { 'class': 'td' }, tok.created ? tok.created.split('T')[0] : '-'), + E('td', { 'class': 'td' }, typeLabel + usedLabel), + E('td', { 'class': 'td' }, deleteBtn) + ]); + }, + + renderCloneRow: function(clone) { + var statusColor = clone.status === 'active' ? '#22c55e' : '#f59e0b'; + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, clone.info || '-'), + E('td', { 'class': 'td' }, E('span', { 'style': 'color:' + statusColor }, clone.status)) + ]); + }, + + handleBuild: function() { + ui.showModal('Build Clone Image', [ + E('p', {}, 'This will build a clone image of the current system.'), + E('p', {}, 'The image can then be flashed to other devices of the same type.'), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, 'Cancel'), + ' ', + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': L.bind(function() { + ui.hideModal(); + callBuildImage().then(L.bind(function(res) { + ui.addNotification(null, E('p', res.message || 'Build started'), 'info'); + }, this)); + }, this) }, 'Build') + ]) + ]); + }, + + handleTftp: function(start) { + var fn = start ? callTftpStart : callTftpStop; + fn().then(L.bind(function(res) { + ui.addNotification(null, E('p', res.message || (start ? 'TFTP started' : 'TFTP stopped')), 'info'); + this.refresh(); + }, this)); + }, + + handleNewToken: function() { + callGenerateToken(false).then(L.bind(function(res) { + if (res.success) { + ui.showModal('Token Generated', [ + E('p', {}, 'New clone token created:'), + E('pre', { 'style': 'background:#f1f5f9;padding:10px;border-radius:4px;word-break:break-all;' }, res.token), + E('p', { 'style': 'color:#888;' }, 'This token requires manual approval when used.'), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { + ui.hideModal(); + } }, 'OK') + ]) + ]); + this.refresh(); + } + }, this)); + }, + + handleAutoToken: function() { + callGenerateToken(true).then(L.bind(function(res) { + if (res.success) { + ui.showModal('Auto-Approve Token Generated', [ + E('p', {}, 'New auto-approve token created:'), + E('pre', { 'style': 'background:#22c55e22;padding:10px;border-radius:4px;word-break:break-all;' }, res.token), + E('p', { 'style': 'color:#22c55e;' }, 'Devices using this token will auto-join the mesh without manual approval.'), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { + ui.hideModal(); + } }, 'OK') + ]) + ]); + this.refresh(); + } + }, this)); + }, + + handleDeleteToken: function(ev) { + var token = ev.currentTarget.dataset.token; + if (confirm('Delete this token?')) { + callDeleteToken(token).then(L.bind(function() { + this.refresh(); + }, this)); + } + }, + + refresh: function() { + return Promise.all([ + callGetStatus(), + callListImages(), + callListTokens(), + callListClones() + ]).then(L.bind(function(data) { + var status = data[0] || {}; + var tokens = data[2].tokens || []; + + // Update counts + var tftpEl = document.getElementById('tftp-status'); + var tokenEl = document.getElementById('token-count'); + var cloneEl = document.getElementById('clone-count'); + + if (tftpEl) { + tftpEl.textContent = status.tftp_running ? 'Running' : 'Stopped'; + tftpEl.parentNode.parentNode.className = status.tftp_running ? 'tftp-on' : 'tftp-off'; + } + if (tokenEl) tokenEl.textContent = String(tokens.length); + if (cloneEl) cloneEl.textContent = String(status.clone_count || 0); + + }, this)); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner b/package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner new file mode 100755 index 00000000..939a9f5b --- /dev/null +++ b/package/secubox/luci-app-cloner/root/usr/libexec/rpcd/luci.cloner @@ -0,0 +1,326 @@ +#!/bin/sh +# +# RPCD handler for SecuBox Cloner +# + +. /usr/share/libubox/jshn.sh + +CLONE_DIR="/srv/secubox/clone" +TFTP_ROOT="/srv/tftp" +TOKENS_DIR="/var/run/secubox/clone-tokens" +STATE_FILE="/var/run/secubox/cloner.state" + +# Detect device type +detect_device() { + local board_name="" + [ -f /tmp/sysinfo/board_name ] && board_name=$(cat /tmp/sysinfo/board_name) + [ -z "$board_name" ] && board_name=$(uci -q get system.@system[0].hostname 2>/dev/null) + + case "$board_name" in + *mochabin*|*MOCHAbin*|globalscale,mochabin) echo "mochabin" ;; + *espressobin*ultra*) echo "espressobin-ultra" ;; + *espressobin*) echo "espressobin-v7" ;; + *x86*|*generic*) echo "x86-64" ;; + *) echo "unknown" ;; + esac +} + +get_lan_ip() { + uci -q get network.lan.ipaddr 2>/dev/null || echo "192.168.255.1" +} + +do_status() { + local device_type lan_ip hostname tftp_enabled + local has_image image_size image_name token_count clone_count + + json_init + + # Device info + device_type=$(detect_device) + lan_ip=$(get_lan_ip) + hostname=$(uci -q get system.@system[0].hostname || echo "secubox") + + json_add_string "device_type" "$device_type" + json_add_string "lan_ip" "$lan_ip" + json_add_string "hostname" "$hostname" + + # TFTP status + tftp_enabled=$(uci -q get dhcp.@dnsmasq[0].enable_tftp) + json_add_boolean "tftp_running" "$([ "$tftp_enabled" = "1" ] && echo 1 || echo 0)" + json_add_string "tftp_root" "$TFTP_ROOT" + + # Image status + has_image=0 + image_size="" + image_name="" + if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then + has_image=1 + image_size=$(ls -lh "$TFTP_ROOT/secubox-clone.img" 2>/dev/null | awk '{print $5}') + image_name="secubox-clone.img" + fi + json_add_boolean "has_image" "$has_image" + json_add_string "image_size" "${image_size:-0}" + json_add_string "image_name" "${image_name:-}" + + # Token count + token_count=0 + [ -d "$TOKENS_DIR" ] && token_count=$(ls "$TOKENS_DIR"/*.json 2>/dev/null | wc -l) + json_add_int "token_count" "$token_count" + + # Clone count (from master-link peer-list) + clone_count=0 + if [ -x /usr/lib/secubox/master-link.sh ]; then + clone_count=$(/usr/lib/secubox/master-link.sh peer-list 2>/dev/null | grep -c "^[0-9]" || echo 0) + fi + json_add_int "clone_count" "$clone_count" + + # Build state + if [ -f "$STATE_FILE" ]; then + . "$STATE_FILE" + json_add_string "last_build" "${BUILD_TIME:-}" + else + json_add_string "last_build" "" + fi + + json_dump +} + +do_list_images() { + local img name + + json_init + json_add_array "images" + + # TFTP ready image + if [ -f "$TFTP_ROOT/secubox-clone.img" ]; then + json_add_object "" + json_add_string "name" "secubox-clone.img" + json_add_string "path" "$TFTP_ROOT/secubox-clone.img" + json_add_string "size" "$(ls -lh "$TFTP_ROOT/secubox-clone.img" | awk '{print $5}')" + json_add_boolean "tftp_ready" 1 + json_close_object + fi + + # Clone directory images + if [ -d "$CLONE_DIR" ]; then + for img in "$CLONE_DIR"/*.img "$CLONE_DIR"/*.img.gz; do + [ -f "$img" ] || continue + name=$(basename "$img") + json_add_object "" + json_add_string "name" "$name" + json_add_string "path" "$img" + json_add_string "size" "$(ls -lh "$img" | awk '{print $5}')" + json_add_boolean "tftp_ready" 0 + json_close_object + done + fi + + json_close_array + json_dump +} + +do_list_tokens() { + local tf token created used auto + + json_init + json_add_array "tokens" + + if [ -d "$TOKENS_DIR" ]; then + for tf in "$TOKENS_DIR"/*.json; do + [ -f "$tf" ] || continue + token=$(jsonfilter -i "$tf" -e '@.token' 2>/dev/null) + created=$(jsonfilter -i "$tf" -e '@.created' 2>/dev/null) + used=$(jsonfilter -i "$tf" -e '@.used' 2>/dev/null) + auto=$(jsonfilter -i "$tf" -e '@.auto_approve' 2>/dev/null) + + json_add_object "" + json_add_string "token" "$token" + json_add_string "token_short" "${token:0:16}..." + json_add_string "created" "$created" + json_add_boolean "used" "$([ "$used" = "true" ] && echo 1 || echo 0)" + json_add_boolean "auto_approve" "$([ "$auto" = "true" ] && echo 1 || echo 0)" + json_close_object + done + fi + + json_close_array + json_dump +} + +do_list_clones() { + local peer_ip peer_name peer_status + + json_init + json_add_array "clones" + + # Get peer list from WireGuard interfaces (most reliable source) + # Each wg peer is a potential clone + for wg in /etc/config/network; do + # Get WireGuard peers from UCI + uci -q show network 2>/dev/null | grep "\.public_key=" | while read -r line; do + peer_name=$(echo "$line" | cut -d'.' -f2) + # Skip if not a wireguard peer + echo "$peer_name" | grep -q "^wg" || continue + peer_ip=$(uci -q get "network.${peer_name}.endpoint_host" 2>/dev/null) + [ -n "$peer_ip" ] || continue + json_add_object "" + json_add_string "info" "$peer_name ($peer_ip)" + json_add_string "name" "$peer_name" + json_add_string "ip" "$peer_ip" + json_add_string "status" "active" + json_close_object + done + break # Only need to run once + done + + # Also check master-link peer-list if available + if [ -x /usr/lib/secubox/master-link.sh ]; then + /usr/lib/secubox/master-link.sh peer-list 2>/dev/null | grep "^[0-9]" > /tmp/cloner_peers.tmp 2>/dev/null + while read -r line; do + [ -n "$line" ] || continue + json_add_object "" + json_add_string "info" "$line" + json_add_string "status" "mesh" + json_close_object + done < /tmp/cloner_peers.tmp 2>/dev/null + rm -f /tmp/cloner_peers.tmp + fi + + json_close_array + json_dump +} + +do_generate_token() { + local input auto_approve token token_file + + read input + auto_approve=$(echo "$input" | jsonfilter -e '@.auto_approve' 2>/dev/null) + + mkdir -p "$TOKENS_DIR" + token=$(head -c 32 /dev/urandom | sha256sum | cut -d' ' -f1) + token_file="$TOKENS_DIR/${token}.json" + + cat > "$token_file" <&1 > /tmp/cloner-build.log) & + json_add_boolean "success" 1 + json_add_string "message" "Build started in background" + else + json_add_boolean "success" 0 + json_add_string "message" "secubox-cloner not installed" + fi + + json_dump +} + +do_tftp_start() { + json_init + + uci -q set dhcp.@dnsmasq[0].enable_tftp='1' + uci -q set dhcp.@dnsmasq[0].tftp_root="$TFTP_ROOT" + uci commit dhcp + /etc/init.d/dnsmasq restart 2>/dev/null + + json_add_boolean "success" 1 + json_add_string "message" "TFTP server started" + json_dump +} + +do_tftp_stop() { + json_init + + uci -q set dhcp.@dnsmasq[0].enable_tftp='0' + uci commit dhcp + /etc/init.d/dnsmasq restart 2>/dev/null + + json_add_boolean "success" 1 + json_add_string "message" "TFTP server stopped" + json_dump +} + +do_delete_token() { + local input token + + read input + token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null) + + json_init + if [ -n "$token" ] && [ -f "$TOKENS_DIR/${token}.json" ]; then + rm -f "$TOKENS_DIR/${token}.json" + json_add_boolean "success" 1 + json_add_string "message" "Token deleted" + else + json_add_boolean "success" 0 + json_add_string "message" "Token not found" + fi + json_dump +} + +do_delete_image() { + local input name + + read input + name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null) + + json_init + if [ -n "$name" ]; then + rm -f "$CLONE_DIR/$name" "$TFTP_ROOT/$name" 2>/dev/null + json_add_boolean "success" 1 + json_add_string "message" "Image deleted" + else + json_add_boolean "success" 0 + json_add_string "message" "Image not found" + fi + json_dump +} + +case "$1" in + list) + echo '{' + echo '"status":{},' + echo '"list_images":{},' + echo '"list_tokens":{},' + echo '"list_clones":{},' + echo '"generate_token":{"auto_approve":"Boolean"},' + echo '"build_image":{},' + echo '"tftp_start":{},' + echo '"tftp_stop":{},' + echo '"delete_token":{"token":"String"},' + echo '"delete_image":{"name":"String"}' + echo '}' + ;; + call) + case "$2" in + status) do_status ;; + list_images) do_list_images ;; + list_tokens) do_list_tokens ;; + list_clones) do_list_clones ;; + generate_token) do_generate_token ;; + build_image) do_build_image ;; + tftp_start) do_tftp_start ;; + tftp_stop) do_tftp_stop ;; + delete_token) do_delete_token ;; + delete_image) do_delete_image ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-cloner/root/usr/share/luci/menu.d/luci-app-cloner.json b/package/secubox/luci-app-cloner/root/usr/share/luci/menu.d/luci-app-cloner.json new file mode 100644 index 00000000..b0f81288 --- /dev/null +++ b/package/secubox/luci-app-cloner/root/usr/share/luci/menu.d/luci-app-cloner.json @@ -0,0 +1,13 @@ +{ + "admin/secubox/system/cloner": { + "title": "Cloning Station", + "order": 50, + "action": { + "type": "view", + "path": "cloner/overview" + }, + "depends": { + "acl": ["luci-app-cloner"] + } + } +} diff --git a/package/secubox/luci-app-cloner/root/usr/share/rpcd/acl.d/luci-app-cloner.json b/package/secubox/luci-app-cloner/root/usr/share/rpcd/acl.d/luci-app-cloner.json new file mode 100644 index 00000000..adcb9429 --- /dev/null +++ b/package/secubox/luci-app-cloner/root/usr/share/rpcd/acl.d/luci-app-cloner.json @@ -0,0 +1,15 @@ +{ + "luci-app-cloner": { + "description": "Grant access to SecuBox Cloning Station", + "read": { + "ubus": { + "luci.cloner": ["status", "list_images", "list_tokens", "list_clones"] + } + }, + "write": { + "ubus": { + "luci.cloner": ["generate_token", "build_image", "tftp_start", "tftp_stop", "delete_token", "delete_image"] + } + } + } +}