diff --git a/.claude/WIP.md b/.claude/WIP.md index 150177de..46ba2105 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -1,6 +1,6 @@ # Work In Progress (Claude) -_Last updated: 2026-03-08 (Maegia Domains Fix)_ +_Last updated: 2026-03-08 (RTTY Remote Module)_ > **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches @@ -10,6 +10,21 @@ _Last updated: 2026-03-08 (Maegia Domains Fix)_ ### 2026-03-08 +- **RTTY Remote Control Module (Phase 1 - RPCD Proxy)** + - Backend: `secubox-app-rtty-remote` with `rttyctl` CLI + - Frontend: `luci-app-rtty-remote` with KISS dashboard + - RPCD Proxy: Execute remote ubus calls to mesh nodes over HTTP + - CLI commands: `rttyctl nodes/rpc/rpc-list/rpc-batch/auth/sessions` + - RPCD methods: status, get_nodes, rpc_call, rpc_list, get_sessions, connect + - Session tracking with SQLite database + - Master-link integration for authentication + - Tested: `rttyctl rpc 127.0.0.1 system board` works + +- **HAProxy mitmproxy Port Fix** + - Changed mitmproxy-in WAF port from 8890 to 22222 + - Fixed UCI config regeneration issue (was overwriting manual edits) + - All vhosts now routing correctly through WAF + - **Vortex DNS Zone Management & Secondary DNS** - Added zone commands: `vortexctl zone list/dump/import/export/reload` - Added secondary DNS commands: `vortexctl secondary list/add/remove` diff --git a/.claude/settings.local.json b/.claude/settings.local.json index a29fc05d..6caf4a7a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -515,7 +515,9 @@ "Bash(./scripts/check-sbom-prereqs.sh:*)", "Bash(git revert:*)", "Bash(__NEW_LINE_17d05a792c15c52e__ echo \"\")", - "Bash(__NEW_LINE_9054b2ef7cdd675f__ echo \"\")" + "Bash(__NEW_LINE_9054b2ef7cdd675f__ echo \"\")", + "Bash(do echo -n \"$host: \")", + "Bash(do echo -n \"$d: \")" ] } } diff --git a/package/secubox/luci-app-rtty-remote/Makefile b/package/secubox/luci-app-rtty-remote/Makefile new file mode 100644 index 00000000..04e9dbf6 --- /dev/null +++ b/package/secubox/luci-app-rtty-remote/Makefile @@ -0,0 +1,16 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-rtty-remote +PKG_VERSION:=0.1.0 +PKG_RELEASE:=1 +PKG_LICENSE:=GPL-3.0 +PKG_MAINTAINER:=SecuBox Team + +LUCI_TITLE:=LuCI RTTY Remote Control Dashboard +LUCI_DEPENDS:=+secubox-app-rtty-remote +luci-base +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildance +$(eval $(call BuildPackage,luci-app-rtty-remote)) diff --git a/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/dashboard.js b/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/dashboard.js new file mode 100644 index 00000000..980c6e66 --- /dev/null +++ b/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/dashboard.js @@ -0,0 +1,349 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; + +var callStatus = rpc.declare({ + object: 'luci.rtty-remote', + method: 'status', + expect: {} +}); + +var callGetNodes = rpc.declare({ + object: 'luci.rtty-remote', + method: 'get_nodes', + expect: {} +}); + +var callGetNode = rpc.declare({ + object: 'luci.rtty-remote', + method: 'get_node', + params: ['node_id'], + expect: {} +}); + +var callRpcCall = rpc.declare({ + object: 'luci.rtty-remote', + method: 'rpc_call', + params: ['node_id', 'object', 'method', 'params'], + expect: {} +}); + +var callRpcList = rpc.declare({ + object: 'luci.rtty-remote', + method: 'rpc_list', + params: ['node_id'], + expect: {} +}); + +var callGetSessions = rpc.declare({ + object: 'luci.rtty-remote', + method: 'get_sessions', + params: ['node_id', 'limit'], + expect: {} +}); + +var callServerStart = rpc.declare({ + object: 'luci.rtty-remote', + method: 'server_start', + expect: {} +}); + +var callServerStop = rpc.declare({ + object: 'luci.rtty-remote', + method: 'server_stop', + expect: {} +}); + +var callConnect = rpc.declare({ + object: 'luci.rtty-remote', + method: 'connect', + params: ['node_id'], + expect: {} +}); + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return Promise.all([ + callStatus(), + callGetNodes(), + callGetSessions(null, 20) + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var nodesData = data[1] || {}; + var sessionsData = data[2] || []; + + var nodes = nodesData.nodes || []; + var sessions = Array.isArray(sessionsData) ? sessionsData : []; + + var view = E('div', { 'class': 'cbi-map' }, [ + this.renderHeader(status), + this.renderStats(status), + this.renderNodesSection(nodes), + this.renderSessionsSection(sessions), + this.renderRpcConsole() + ]); + + // Start polling + poll.add(L.bind(this.pollStatus, this), 10); + + return view; + }, + + renderHeader: function(status) { + var serverStatus = status.running ? + E('span', { 'style': 'color: #0a0; font-weight: bold;' }, 'RUNNING') : + E('span', { 'style': 'color: #a00; font-weight: bold;' }, 'STOPPED'); + + var toggleBtn = status.running ? + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': L.bind(this.handleServerStop, this) + }, 'Stop Server') : + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': L.bind(this.handleServerStart, this) + }, 'Start Server'); + + return E('div', { 'class': 'cbi-section' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [ + E('h2', { 'style': 'margin: 0;' }, 'RTTY Remote Control'), + E('div', { 'style': 'display: flex; align-items: center; gap: 1em;' }, [ + E('span', {}, ['Server: ', serverStatus]), + toggleBtn + ]) + ]), + E('p', { 'style': 'color: #666; margin: 0;' }, + 'Remote control assistant for SecuBox mesh nodes. Execute RPCD calls, manage terminals, and replay sessions.') + ]); + }, + + renderStats: function(status) { + var stats = [ + { label: 'NODES', value: status.unique_nodes || 0, color: '#4a9' }, + { label: 'SESSIONS', value: status.total_sessions || 0, color: '#49a' }, + { label: 'ACTIVE', value: status.active_sessions || 0, color: '#a94' }, + { label: 'RPC CALLS', value: status.total_rpc_calls || 0, color: '#94a' } + ]; + + return E('div', { 'class': 'cbi-section', 'style': 'display: flex; gap: 1em; flex-wrap: wrap;' }, + stats.map(function(stat) { + return E('div', { + 'style': 'flex: 1; min-width: 120px; padding: 1em; background: #f5f5f5; border-radius: 8px; text-align: center; border-left: 4px solid ' + stat.color + ';' + }, [ + E('div', { 'style': 'font-size: 2em; font-weight: bold; color: #333;' }, String(stat.value)), + E('div', { 'style': 'font-size: 0.9em; color: #666; text-transform: uppercase;' }, stat.label) + ]); + }) + ); + }, + + renderNodesSection: function(nodes) { + var self = this; + + var table = E('table', { 'class': 'table', 'id': 'nodes-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Node ID'), + E('th', { 'class': 'th' }, 'Name'), + E('th', { 'class': 'th' }, 'Address'), + E('th', { 'class': 'th' }, 'Status'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ]); + + if (nodes.length === 0) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'colspan': '5', 'style': 'text-align: center; color: #666;' }, + 'No mesh nodes found. Connect nodes via Master-Link or P2P.') + ])); + } else { + nodes.forEach(function(node) { + var statusBadge = node.status === 'approved' || node.status === 'online' ? + E('span', { 'style': 'color: #0a0;' }, 'ONLINE') : + E('span', { 'style': 'color: #a00;' }, node.status || 'OFFLINE'); + + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, E('code', {}, node.id || '-')), + E('td', { 'class': 'td' }, node.name || '-'), + E('td', { 'class': 'td' }, E('code', {}, node.address || '-')), + E('td', { 'class': 'td' }, statusBadge), + E('td', { 'class': 'td' }, [ + E('button', { + 'class': 'cbi-button cbi-button-action', + 'style': 'margin-right: 0.5em;', + 'click': L.bind(self.handleRpcConsoleOpen, self, node) + }, 'RPC'), + E('button', { + 'class': 'cbi-button', + 'click': L.bind(self.handleConnect, self, node) + }, 'Term') + ]) + ])); + }); + } + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Connected Nodes'), + table + ]); + }, + + renderSessionsSection: function(sessions) { + var table = E('table', { 'class': 'table', 'id': 'sessions-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'ID'), + E('th', { 'class': 'th' }, 'Node'), + E('th', { 'class': 'th' }, 'Type'), + E('th', { 'class': 'th' }, 'Started'), + E('th', { 'class': 'th' }, 'Duration'), + E('th', { 'class': 'th' }, 'Label') + ]) + ]); + + if (!sessions || sessions.length === 0) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'colspan': '6', 'style': 'text-align: center; color: #666;' }, + 'No sessions recorded yet.') + ])); + } else { + sessions.forEach(function(session) { + var duration = session.duration ? (session.duration + 's') : 'active'; + + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, String(session.id)), + E('td', { 'class': 'td' }, session.node_id || '-'), + E('td', { 'class': 'td' }, session.type || '-'), + E('td', { 'class': 'td' }, session.started || '-'), + E('td', { 'class': 'td' }, duration), + E('td', { 'class': 'td' }, session.label || '-') + ])); + }); + } + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Recent Sessions'), + table + ]); + }, + + renderRpcConsole: function() { + return E('div', { 'class': 'cbi-section', 'id': 'rpc-console' }, [ + E('h3', {}, 'RPC Console'), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 0.5em; margin-bottom: 1em;' }, [ + E('input', { + 'type': 'text', + 'id': 'rpc-node', + 'placeholder': 'Node (IP or ID)', + 'class': 'cbi-input-text' + }), + E('input', { + 'type': 'text', + 'id': 'rpc-object', + 'placeholder': 'Object (e.g., luci.system-hub)', + 'class': 'cbi-input-text' + }), + E('input', { + 'type': 'text', + 'id': 'rpc-method', + 'placeholder': 'Method (e.g., status)', + 'class': 'cbi-input-text' + }), + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': L.bind(this.handleRpcExecute, this) + }, 'Execute') + ]), + E('div', { 'style': 'margin-bottom: 0.5em;' }, [ + E('input', { + 'type': 'text', + 'id': 'rpc-params', + 'placeholder': 'Parameters (JSON, optional)', + 'class': 'cbi-input-text', + 'style': 'width: 100%;' + }) + ]), + E('pre', { + 'id': 'rpc-result', + 'style': 'background: #1a1a2e; color: #0f0; padding: 1em; border-radius: 4px; min-height: 150px; overflow: auto; font-family: monospace;' + }, '// RPC result will appear here...') + ]); + }, + + handleServerStart: function() { + var self = this; + callServerStart().then(function() { + ui.addNotification(null, E('p', 'Server started'), 'success'); + self.pollStatus(); + }).catch(function(err) { + ui.addNotification(null, E('p', 'Failed to start server: ' + err.message), 'error'); + }); + }, + + handleServerStop: function() { + var self = this; + callServerStop().then(function() { + ui.addNotification(null, E('p', 'Server stopped'), 'success'); + self.pollStatus(); + }).catch(function(err) { + ui.addNotification(null, E('p', 'Failed to stop server: ' + err.message), 'error'); + }); + }, + + handleConnect: function(node) { + callConnect(node.id || node.address).then(function(result) { + ui.addNotification(null, E('p', [ + 'To connect to ', E('strong', {}, node.name || node.id), ':', E('br'), + E('code', {}, result.ssh_command || ('ssh root@' + node.address)) + ]), 'info'); + }); + }, + + handleRpcConsoleOpen: function(node) { + document.getElementById('rpc-node').value = node.address || node.id; + document.getElementById('rpc-object').value = 'system'; + document.getElementById('rpc-method').value = 'board'; + document.getElementById('rpc-node').scrollIntoView({ behavior: 'smooth' }); + }, + + handleRpcExecute: function() { + var node = document.getElementById('rpc-node').value; + var object = document.getElementById('rpc-object').value; + var method = document.getElementById('rpc-method').value; + var params = document.getElementById('rpc-params').value || '{}'; + var resultEl = document.getElementById('rpc-result'); + + if (!node || !object || !method) { + resultEl.textContent = '// Error: Node, Object, and Method are required'; + return; + } + + resultEl.textContent = '// Executing ' + object + '.' + method + ' on ' + node + '...'; + + callRpcCall(node, object, method, params).then(function(response) { + if (response.success) { + resultEl.textContent = JSON.stringify(response.result, null, 2); + } else { + resultEl.textContent = '// Error: ' + (response.error || 'Unknown error'); + } + }).catch(function(err) { + resultEl.textContent = '// Error: ' + err.message; + }); + }, + + pollStatus: function() { + var self = this; + return callStatus().then(function(status) { + // Update stats display + // (In a full implementation, update the DOM elements) + }); + } +}); diff --git a/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/support.js b/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/support.js new file mode 100644 index 00000000..6f0e7d22 --- /dev/null +++ b/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/support.js @@ -0,0 +1,489 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; +'require fs'; + +var callStatus = rpc.declare({ + object: 'luci.rtty-remote', + method: 'status', + expect: {} +}); + +var callGetNodes = rpc.declare({ + object: 'luci.rtty-remote', + method: 'get_nodes', + expect: {} +}); + +var callConnect = rpc.declare({ + object: 'luci.rtty-remote', + method: 'connect', + params: ['node_id'], + expect: {} +}); + +var callRpcCall = rpc.declare({ + object: 'luci.rtty-remote', + method: 'rpc_call', + params: ['node_id', 'object', 'method', 'params'], + expect: {} +}); + +// Support session management +var supportSessions = {}; + +return view.extend({ + handleSaveApply: null, + handleSave: null, + handleReset: null, + + load: function() { + return Promise.all([ + callStatus(), + callGetNodes() + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var nodesData = data[1] || {}; + var nodes = nodesData.nodes || []; + + var view = E('div', { 'class': 'cbi-map' }, [ + this.renderHeader(), + this.renderSupportCodeSection(), + this.renderActiveSessionsSection(), + this.renderRemoteNodesSection(nodes), + this.renderTerminalSection(), + this.renderSharedControlsSection() + ]); + + // Start polling for session updates + poll.add(L.bind(this.pollSessions, this), 5); + + return view; + }, + + renderHeader: function() { + return E('div', { 'class': 'cbi-section' }, [ + E('h2', { 'style': 'margin: 0;' }, 'Remote Support Panel'), + E('p', { 'style': 'color: #666;' }, + 'Provide or receive remote assistance with authenticated console sessions and shared controls.') + ]); + }, + + renderSupportCodeSection: function() { + var self = this; + + // Generate a random support code + var supportCode = this.generateSupportCode(); + + return E('div', { 'class': 'cbi-section', 'style': 'background: #f0f8ff; border: 2px dashed #4a9; padding: 1.5em; border-radius: 8px;' }, [ + E('h3', { 'style': 'margin-top: 0;' }, 'Support Session'), + E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 2em;' }, [ + // Provide support section + E('div', {}, [ + E('h4', {}, 'Provide Support'), + E('p', { 'style': 'font-size: 0.9em; color: #666;' }, 'Enter the support code shared by the user needing assistance.'), + E('div', { 'style': 'display: flex; gap: 0.5em;' }, [ + E('input', { + 'type': 'text', + 'id': 'remote-support-code', + 'placeholder': 'Enter support code (e.g., SB-XXXX)', + 'class': 'cbi-input-text', + 'style': 'flex: 1; font-family: monospace; text-transform: uppercase;' + }), + E('button', { + 'class': 'cbi-button cbi-button-positive', + 'click': L.bind(this.handleJoinSession, this) + }, 'Connect') + ]) + ]), + // Request support section + E('div', {}, [ + E('h4', {}, 'Request Support'), + E('p', { 'style': 'font-size: 0.9em; color: #666;' }, 'Share this code with your support technician.'), + E('div', { 'style': 'display: flex; gap: 0.5em; align-items: center;' }, [ + E('code', { + 'id': 'my-support-code', + 'style': 'font-size: 1.5em; padding: 0.5em 1em; background: #fff; border: 1px solid #ccc; border-radius: 4px; font-family: monospace;' + }, supportCode), + E('button', { + 'class': 'cbi-button', + 'click': L.bind(this.handleCopyCode, this, supportCode) + }, 'Copy'), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': L.bind(this.handleStartSupportSession, this, supportCode) + }, 'Start Session') + ]) + ]) + ]) + ]); + }, + + renderActiveSessionsSection: function() { + return E('div', { 'class': 'cbi-section', 'id': 'active-sessions-section', 'style': 'display: none;' }, [ + E('h3', {}, 'Active Support Sessions'), + E('div', { 'id': 'active-sessions-list' }, [ + E('p', { 'style': 'color: #666;' }, 'No active sessions') + ]) + ]); + }, + + renderRemoteNodesSection: function(nodes) { + var self = this; + + if (nodes.length === 0) { + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Quick Connect'), + E('p', { 'style': 'color: #666;' }, 'No mesh nodes available. Enter IP address manually:'), + E('div', { 'style': 'display: flex; gap: 0.5em;' }, [ + E('input', { + 'type': 'text', + 'id': 'manual-node-ip', + 'placeholder': 'IP Address (e.g., 192.168.1.1)', + 'class': 'cbi-input-text' + }), + E('button', { + 'class': 'cbi-button cbi-button-action', + 'click': L.bind(this.handleQuickConnect, this) + }, 'Connect') + ]) + ]); + } + + var nodeButtons = nodes.map(function(node) { + return E('button', { + 'class': 'cbi-button', + 'style': 'margin: 0.25em;', + 'click': L.bind(self.handleNodeConnect, self, node) + }, [ + E('strong', {}, node.name || node.id), + E('br'), + E('small', {}, node.address || 'unknown') + ]); + }); + + return E('div', { 'class': 'cbi-section' }, [ + E('h3', {}, 'Quick Connect'), + E('div', { 'style': 'display: flex; flex-wrap: wrap;' }, nodeButtons) + ]); + }, + + renderTerminalSection: function() { + return E('div', { 'class': 'cbi-section', 'id': 'terminal-section', 'style': 'display: none;' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center;' }, [ + E('h3', { 'style': 'margin: 0;' }, [ + 'Terminal: ', + E('span', { 'id': 'terminal-target' }, 'Not connected') + ]), + E('div', {}, [ + E('button', { + 'class': 'cbi-button', + 'id': 'btn-share-control', + 'click': L.bind(this.handleShareControl, this) + }, 'Share Control'), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'margin-left: 0.5em;', + 'click': L.bind(this.handleDisconnect, this) + }, 'Disconnect') + ]) + ]), + E('div', { + 'id': 'terminal-container', + 'style': 'background: #1a1a2e; color: #0f0; padding: 1em; border-radius: 4px; min-height: 400px; font-family: monospace; overflow: auto;' + }, [ + E('div', { 'id': 'terminal-output', 'style': 'white-space: pre-wrap;' }, ''), + E('div', { 'style': 'display: flex; margin-top: 0.5em;' }, [ + E('span', { 'style': 'color: #0f0;' }, '$ '), + E('input', { + 'type': 'text', + 'id': 'terminal-input', + 'style': 'flex: 1; background: transparent; border: none; color: #0f0; font-family: monospace; outline: none;', + 'autocomplete': 'off', + 'keydown': L.bind(this.handleTerminalKeydown, this) + }) + ]) + ]) + ]); + }, + + renderSharedControlsSection: function() { + return E('div', { 'class': 'cbi-section', 'id': 'shared-controls-section', 'style': 'display: none;' }, [ + E('h3', {}, 'Shared Controls'), + E('p', { 'style': 'color: #666;' }, 'Quick actions for remote node management.'), + E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.5em;' }, [ + E('button', { 'class': 'cbi-button', 'click': L.bind(this.handleQuickAction, this, 'system', 'board') }, 'System Info'), + E('button', { 'class': 'cbi-button', 'click': L.bind(this.handleQuickAction, this, 'network.interface', 'dump') }, 'Network Info'), + E('button', { 'class': 'cbi-button', 'click': L.bind(this.handleQuickAction, this, 'luci.system-hub', 'status') }, 'Service Status'), + E('button', { 'class': 'cbi-button', 'click': L.bind(this.handleQuickAction, this, 'luci.haproxy', 'vhost_list') }, 'Vhosts'), + E('button', { 'class': 'cbi-button cbi-button-action', 'click': L.bind(this.handleQuickAction, this, 'system', 'info') }, 'Memory/Load'), + E('button', { 'class': 'cbi-button cbi-button-negative', 'click': L.bind(this.handleReboot, this) }, 'Reboot') + ]) + ]); + }, + + // Utility functions + generateSupportCode: function() { + var chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + var code = 'SB-'; + for (var i = 0; i < 4; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return code; + }, + + // Event handlers + handleCopyCode: function(code) { + navigator.clipboard.writeText(code).then(function() { + ui.addNotification(null, E('p', 'Support code copied to clipboard'), 'success'); + }); + }, + + handleStartSupportSession: function(code) { + supportSessions[code] = { + started: Date.now(), + node: 'local', + status: 'waiting' + }; + + document.getElementById('active-sessions-section').style.display = 'block'; + this.updateSessionsList(); + + ui.addNotification(null, E('p', [ + 'Support session started with code: ', + E('strong', {}, code), + E('br'), + 'Share this code with your support technician.' + ]), 'info'); + }, + + handleJoinSession: function() { + var code = document.getElementById('remote-support-code').value.toUpperCase().trim(); + + if (!code || !code.match(/^SB-[A-Z0-9]{4}$/)) { + ui.addNotification(null, E('p', 'Invalid support code format. Use SB-XXXX'), 'error'); + return; + } + + ui.addNotification(null, E('p', 'Connecting to support session: ' + code), 'info'); + + // In a real implementation, this would connect via WebSocket/RTTY + // For now, simulate connection + supportSessions[code] = { + started: Date.now(), + node: 'remote', + status: 'connected' + }; + + document.getElementById('active-sessions-section').style.display = 'block'; + this.updateSessionsList(); + }, + + handleQuickConnect: function() { + var ip = document.getElementById('manual-node-ip').value.trim(); + if (!ip) { + ui.addNotification(null, E('p', 'Please enter an IP address'), 'error'); + return; + } + this.connectToNode(ip, ip); + }, + + handleNodeConnect: function(node) { + this.connectToNode(node.address || node.id, node.name || node.id); + }, + + connectToNode: function(address, name) { + var self = this; + + document.getElementById('terminal-section').style.display = 'block'; + document.getElementById('shared-controls-section').style.display = 'block'; + document.getElementById('terminal-target').textContent = name + ' (' + address + ')'; + document.getElementById('terminal-output').textContent = ''; + + // Store current connection + this.currentNode = address; + + // Test connection with system board call + this.appendTerminal('Connecting to ' + address + '...\n'); + + callRpcCall(address, 'system', 'board', '{}').then(function(result) { + if (result.success) { + self.appendTerminal('Connected to: ' + JSON.stringify(result.result, null, 2) + '\n\n'); + self.appendTerminal('Type commands or use Quick Actions below.\n'); + document.getElementById('terminal-input').focus(); + } else { + self.appendTerminal('Connection failed: ' + (result.error || 'Unknown error') + '\n'); + } + }).catch(function(err) { + self.appendTerminal('Connection error: ' + err.message + '\n'); + }); + }, + + handleDisconnect: function() { + this.currentNode = null; + document.getElementById('terminal-section').style.display = 'none'; + document.getElementById('shared-controls-section').style.display = 'none'; + ui.addNotification(null, E('p', 'Disconnected from remote node'), 'info'); + }, + + handleShareControl: function() { + if (!this.currentNode) return; + + var code = this.generateSupportCode(); + supportSessions[code] = { + started: Date.now(), + node: this.currentNode, + status: 'sharing' + }; + + document.getElementById('active-sessions-section').style.display = 'block'; + this.updateSessionsList(); + + ui.addNotification(null, E('p', [ + 'Shared control session created: ', + E('strong', {}, code), + E('br'), + 'Others can join using this code.' + ]), 'success'); + }, + + handleTerminalKeydown: function(ev) { + if (ev.key !== 'Enter') return; + + var input = document.getElementById('terminal-input'); + var cmd = input.value.trim(); + input.value = ''; + + if (!cmd) return; + + this.appendTerminal('$ ' + cmd + '\n'); + + if (!this.currentNode) { + this.appendTerminal('Error: Not connected to any node\n'); + return; + } + + // Parse command: object.method [params] + var parts = cmd.split(/\s+/); + var objMethod = parts[0].split('.'); + var params = parts.slice(1).join(' ') || '{}'; + + if (objMethod.length < 2) { + this.appendTerminal('Usage: object.method [params_json]\n'); + this.appendTerminal('Example: system.board {}\n'); + return; + } + + var self = this; + var object = objMethod.slice(0, -1).join('.'); + var method = objMethod[objMethod.length - 1]; + + callRpcCall(this.currentNode, object, method, params).then(function(result) { + if (result.success) { + self.appendTerminal(JSON.stringify(result.result, null, 2) + '\n\n'); + } else { + self.appendTerminal('Error: ' + (result.error || 'Unknown error') + '\n\n'); + } + }).catch(function(err) { + self.appendTerminal('Error: ' + err.message + '\n\n'); + }); + }, + + handleQuickAction: function(object, method) { + if (!this.currentNode) { + ui.addNotification(null, E('p', 'Not connected to any node'), 'error'); + return; + } + + var self = this; + this.appendTerminal('$ ' + object + '.' + method + '\n'); + + callRpcCall(this.currentNode, object, method, '{}').then(function(result) { + if (result.success) { + self.appendTerminal(JSON.stringify(result.result, null, 2) + '\n\n'); + } else { + self.appendTerminal('Error: ' + (result.error || 'Unknown error') + '\n\n'); + } + }).catch(function(err) { + self.appendTerminal('Error: ' + err.message + '\n\n'); + }); + }, + + handleReboot: function() { + if (!this.currentNode) { + ui.addNotification(null, E('p', 'Not connected to any node'), 'error'); + return; + } + + if (!confirm('Are you sure you want to reboot the remote node?')) return; + + var self = this; + callRpcCall(this.currentNode, 'system', 'reboot', '{}').then(function() { + self.appendTerminal('Reboot command sent. Node will restart.\n'); + ui.addNotification(null, E('p', 'Reboot command sent'), 'warning'); + }); + }, + + appendTerminal: function(text) { + var output = document.getElementById('terminal-output'); + output.textContent += text; + output.scrollTop = output.scrollHeight; + }, + + updateSessionsList: function() { + var container = document.getElementById('active-sessions-list'); + container.innerHTML = ''; + + var codes = Object.keys(supportSessions); + if (codes.length === 0) { + container.appendChild(E('p', { 'style': 'color: #666;' }, 'No active sessions')); + return; + } + + var table = E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Code'), + E('th', { 'class': 'th' }, 'Node'), + E('th', { 'class': 'th' }, 'Status'), + E('th', { 'class': 'th' }, 'Duration'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ]); + + var self = this; + codes.forEach(function(code) { + var session = supportSessions[code]; + var duration = Math.floor((Date.now() - session.started) / 1000); + var minutes = Math.floor(duration / 60); + var seconds = duration % 60; + + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, E('code', {}, code)), + E('td', { 'class': 'td' }, session.node), + E('td', { 'class': 'td' }, session.status), + E('td', { 'class': 'td' }, minutes + 'm ' + seconds + 's'), + E('td', { 'class': 'td' }, [ + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': function() { + delete supportSessions[code]; + self.updateSessionsList(); + } + }, 'End') + ]) + ])); + }); + + container.appendChild(table); + }, + + pollSessions: function() { + this.updateSessionsList(); + } +}); diff --git a/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote b/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote new file mode 100644 index 00000000..1bd8c338 --- /dev/null +++ b/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote @@ -0,0 +1,340 @@ +#!/bin/sh + +# RPCD handler for luci-app-rtty-remote +# Provides RPC interface for RTTY Remote Control + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +# Load libraries +[ -f /usr/lib/secubox/rtty-proxy.sh ] && . /usr/lib/secubox/rtty-proxy.sh +[ -f /usr/lib/secubox/rtty-session.sh ] && . /usr/lib/secubox/rtty-session.sh +[ -f /usr/lib/secubox/rtty-auth.sh ] && . /usr/lib/secubox/rtty-auth.sh + +RTTYCTL="/usr/sbin/rttyctl" + +#------------------------------------------------------------------------------ +# Helper functions +#------------------------------------------------------------------------------ + +get_config() { + local section="$1" + local option="$2" + local default="$3" + + config_load rtty-remote + config_get value "$section" "$option" "$default" + echo "$value" +} + +#------------------------------------------------------------------------------ +# RPC Methods +#------------------------------------------------------------------------------ + +# Get server status +method_status() { + json_init + + config_load rtty-remote + local enabled + config_get enabled main enabled '0' + json_add_boolean "enabled" "$enabled" + + local port + config_get port main server_port '7681' + json_add_int "port" "$port" + + # Check if running + if pgrep -f "rttyctl server-daemon" >/dev/null 2>&1; then + json_add_boolean "running" 1 + else + json_add_boolean "running" 0 + fi + + # Stats + if [ -f /srv/rtty-remote/sessions.db ]; then + local stats=$(session_stats 2>/dev/null) + json_add_int "total_sessions" "$(echo "$stats" | jsonfilter -e '@.total_sessions' 2>/dev/null || echo 0)" + json_add_int "active_sessions" "$(echo "$stats" | jsonfilter -e '@.active_sessions' 2>/dev/null || echo 0)" + json_add_int "total_rpc_calls" "$(echo "$stats" | jsonfilter -e '@.total_rpc_calls' 2>/dev/null || echo 0)" + json_add_int "unique_nodes" "$(echo "$stats" | jsonfilter -e '@.unique_nodes' 2>/dev/null || echo 0)" + else + json_add_int "total_sessions" 0 + json_add_int "active_sessions" 0 + json_add_int "total_rpc_calls" 0 + json_add_int "unique_nodes" 0 + fi + + json_dump +} + +# Get mesh nodes +method_get_nodes() { + local nodes=$($RTTYCTL json-nodes 2>/dev/null) + [ -z "$nodes" ] && nodes='{"nodes":[]}' + echo "$nodes" +} + +# Get single node details +method_get_node() { + local node_id + read -r input + json_load "$input" + json_get_var node_id node_id + + [ -z "$node_id" ] && { + echo '{"error":"Missing node_id"}' + return + } + + # Get node info from master-link or P2P + json_init + json_add_string "node_id" "$node_id" + + # Try to get address + local addr=$($RTTYCTL node "$node_id" 2>&1 | grep "Address:" | awk '{print $2}') + json_add_string "address" "$addr" + + # Check connectivity + if [ -n "$addr" ] && ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then + json_add_string "status" "online" + else + json_add_string "status" "offline" + fi + + json_dump +} + +# Execute remote RPC call +method_rpc_call() { + local node_id object method params + read -r input + json_load "$input" + json_get_var node_id node_id + json_get_var object object + json_get_var method method + json_get_var params params + + [ -z "$node_id" ] || [ -z "$object" ] || [ -z "$method" ] && { + echo '{"error":"Missing required parameters (node_id, object, method)"}' + return + } + + # Execute via rttyctl + local result=$($RTTYCTL rpc "$node_id" "$object" "$method" "$params" 2>&1) + local rc=$? + + json_init + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + # Try to parse result as JSON + if echo "$result" | jsonfilter -e '@' >/dev/null 2>&1; then + json_add_object "result" + # Add result fields + echo "$result" | jsonfilter -e '@' 2>/dev/null + json_close_object + else + json_add_string "result" "$result" + fi + else + json_add_boolean "success" 0 + json_add_string "error" "$result" + fi + json_dump +} + +# List remote RPCD objects +method_rpc_list() { + local node_id + read -r input + json_load "$input" + json_get_var node_id node_id + + [ -z "$node_id" ] && { + echo '{"error":"Missing node_id"}' + return + } + + local result=$($RTTYCTL rpc-list "$node_id" 2>&1) + echo "$result" +} + +# Get sessions +method_get_sessions() { + local node_id limit + read -r input + json_load "$input" + json_get_var node_id node_id + json_get_var limit limit + + [ -z "$limit" ] && limit=50 + + session_list "$node_id" "$limit" +} + +# Start server +method_server_start() { + /etc/init.d/rtty-remote start 2>&1 + json_init + json_add_boolean "success" 1 + json_dump +} + +# Stop server +method_server_stop() { + /etc/init.d/rtty-remote stop 2>&1 + json_init + json_add_boolean "success" 1 + json_dump +} + +# Get settings +method_get_settings() { + json_init + + config_load rtty-remote + + json_add_object "main" + local val + config_get val main enabled '0' + json_add_boolean "enabled" "$val" + config_get val main server_port '7681' + json_add_int "server_port" "$val" + config_get val main auth_method 'master-link' + json_add_string "auth_method" "$val" + config_get val main session_ttl '3600' + json_add_int "session_ttl" "$val" + config_get val main max_sessions '10' + json_add_int "max_sessions" "$val" + json_close_object + + json_add_object "security" + config_get val security require_wg '1' + json_add_boolean "require_wg" "$val" + config_get val security allowed_networks '' + json_add_string "allowed_networks" "$val" + json_close_object + + json_add_object "proxy" + config_get val proxy rpc_timeout '30' + json_add_int "rpc_timeout" "$val" + config_get val proxy batch_limit '50' + json_add_int "batch_limit" "$val" + config_get val proxy cache_ttl '60' + json_add_int "cache_ttl" "$val" + json_close_object + + json_dump +} + +# Set settings +method_set_settings() { + read -r input + + # Parse and apply settings + local enabled=$(echo "$input" | jsonfilter -e '@.main.enabled' 2>/dev/null) + [ -n "$enabled" ] && uci set rtty-remote.main.enabled="$enabled" + + local port=$(echo "$input" | jsonfilter -e '@.main.server_port' 2>/dev/null) + [ -n "$port" ] && uci set rtty-remote.main.server_port="$port" + + local auth=$(echo "$input" | jsonfilter -e '@.main.auth_method' 2>/dev/null) + [ -n "$auth" ] && uci set rtty-remote.main.auth_method="$auth" + + local ttl=$(echo "$input" | jsonfilter -e '@.main.session_ttl' 2>/dev/null) + [ -n "$ttl" ] && uci set rtty-remote.main.session_ttl="$ttl" + + uci commit rtty-remote + + json_init + json_add_boolean "success" 1 + json_dump +} + +# Replay session +method_replay_session() { + local session_id target_node + read -r input + json_load "$input" + json_get_var session_id session_id + json_get_var target_node target_node + + [ -z "$session_id" ] || [ -z "$target_node" ] && { + echo '{"error":"Missing session_id or target_node"}' + return + } + + local result=$($RTTYCTL replay "$session_id" "$target_node" 2>&1) + + json_init + json_add_boolean "success" 1 + json_add_string "message" "$result" + json_dump +} + +# Connect to node (start terminal) +method_connect() { + local node_id + read -r input + json_load "$input" + json_get_var node_id node_id + + [ -z "$node_id" ] && { + echo '{"error":"Missing node_id"}' + return + } + + # Get node address + local addr=$($RTTYCTL node "$node_id" 2>&1 | grep "Address:" | awk '{print $2}') + + json_init + json_add_string "node_id" "$node_id" + json_add_string "address" "$addr" + json_add_string "terminal_url" "ws://localhost:7681/ws" + json_add_string "ssh_command" "ssh root@$addr" + json_dump +} + +#------------------------------------------------------------------------------ +# Main dispatcher +#------------------------------------------------------------------------------ + +case "$1" in + list) + cat << 'EOF' +{ + "status": {}, + "get_nodes": {}, + "get_node": {"node_id": "string"}, + "rpc_call": {"node_id": "string", "object": "string", "method": "string", "params": "string"}, + "rpc_list": {"node_id": "string"}, + "get_sessions": {"node_id": "string", "limit": 50}, + "server_start": {}, + "server_stop": {}, + "get_settings": {}, + "set_settings": {"config": "object"}, + "replay_session": {"session_id": "integer", "target_node": "string"}, + "connect": {"node_id": "string"} +} +EOF + ;; + call) + case "$2" in + status) method_status ;; + get_nodes) method_get_nodes ;; + get_node) method_get_node ;; + rpc_call) method_rpc_call ;; + rpc_list) method_rpc_list ;; + get_sessions) method_get_sessions ;; + server_start) method_server_start ;; + server_stop) method_server_stop ;; + get_settings) method_get_settings ;; + set_settings) method_set_settings ;; + replay_session) method_replay_session ;; + connect) method_connect ;; + *) + echo '{"error":"Unknown method"}' + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-rtty-remote/root/usr/share/luci/menu.d/luci-app-rtty-remote.json b/package/secubox/luci-app-rtty-remote/root/usr/share/luci/menu.d/luci-app-rtty-remote.json new file mode 100644 index 00000000..e9e44908 --- /dev/null +++ b/package/secubox/luci-app-rtty-remote/root/usr/share/luci/menu.d/luci-app-rtty-remote.json @@ -0,0 +1,25 @@ +{ + "admin/services/rtty-remote": { + "title": "RTTY Remote", + "order": 85, + "action": { + "type": "view", + "path": "rtty-remote/dashboard" + }, + "depends": { + "acl": ["luci-app-rtty-remote"], + "uci": { "rtty-remote": true } + } + }, + "admin/services/rtty-remote/support": { + "title": "Support Panel", + "order": 10, + "action": { + "type": "view", + "path": "rtty-remote/support" + }, + "depends": { + "acl": ["luci-app-rtty-remote"] + } + } +} diff --git a/package/secubox/luci-app-rtty-remote/root/usr/share/rpcd/acl.d/luci-rtty-remote.json b/package/secubox/luci-app-rtty-remote/root/usr/share/rpcd/acl.d/luci-rtty-remote.json new file mode 100644 index 00000000..0cb59400 --- /dev/null +++ b/package/secubox/luci-app-rtty-remote/root/usr/share/rpcd/acl.d/luci-rtty-remote.json @@ -0,0 +1,31 @@ +{ + "luci-app-rtty-remote": { + "description": "Grant access to RTTY Remote Control", + "read": { + "ubus": { + "luci.rtty-remote": [ + "status", + "get_nodes", + "get_node", + "rpc_list", + "get_sessions", + "get_settings" + ] + }, + "uci": ["rtty-remote"] + }, + "write": { + "ubus": { + "luci.rtty-remote": [ + "rpc_call", + "server_start", + "server_stop", + "set_settings", + "replay_session", + "connect" + ] + }, + "uci": ["rtty-remote"] + } + } +} diff --git a/package/secubox/secubox-app-rtty-remote/Makefile b/package/secubox/secubox-app-rtty-remote/Makefile new file mode 100644 index 00000000..62f2fde6 --- /dev/null +++ b/package/secubox/secubox-app-rtty-remote/Makefile @@ -0,0 +1,44 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app-rtty-remote +PKG_VERSION:=0.1.0 +PKG_RELEASE:=1 +PKG_LICENSE:=GPL-3.0 +PKG_MAINTAINER:=SecuBox Team + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app-rtty-remote + SECTION:=secubox + CATEGORY:=SecuBox + SUBMENU:=Remote Control + TITLE:=RTTY Remote Control Agent + DEPENDS:=+secubox-core +secubox-master-link +sqlite3-cli +curl +jshn + PKGARCH:=all +endef + +define Package/secubox-app-rtty-remote/description + Remote control assistant for SecuBox mesh nodes. + Provides RPCD proxy, terminal access via RTTY, and session replay. +endef + +define Build/Compile +endef + +define Package/secubox-app-rtty-remote/install + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_CONF) ./files/etc/config/rtty-remote $(1)/etc/config/ + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/etc/init.d/rtty-remote $(1)/etc/init.d/ + + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) ./files/usr/sbin/rttyctl $(1)/usr/sbin/ + + $(INSTALL_DIR) $(1)/usr/lib/secubox + $(INSTALL_DATA) ./files/usr/lib/secubox/rtty-proxy.sh $(1)/usr/lib/secubox/ + $(INSTALL_DATA) ./files/usr/lib/secubox/rtty-session.sh $(1)/usr/lib/secubox/ + $(INSTALL_DATA) ./files/usr/lib/secubox/rtty-auth.sh $(1)/usr/lib/secubox/ +endef + +$(eval $(call BuildPackage,secubox-app-rtty-remote)) diff --git a/package/secubox/secubox-app-rtty-remote/files/etc/config/rtty-remote b/package/secubox/secubox-app-rtty-remote/files/etc/config/rtty-remote new file mode 100644 index 00000000..65ac8657 --- /dev/null +++ b/package/secubox/secubox-app-rtty-remote/files/etc/config/rtty-remote @@ -0,0 +1,18 @@ +config rtty-remote 'main' + option enabled '1' + option server_port '7681' + option auth_method 'master-link' + option session_ttl '3600' + option max_sessions '10' + option log_level 'info' + +config security 'security' + option require_wg '1' + option allowed_networks '10.100.0.0/24 192.168.255.0/24' + option session_encryption '1' + +config proxy 'proxy' + option rpc_timeout '30' + option batch_limit '50' + option cache_ttl '60' + option http_port '8080' diff --git a/package/secubox/secubox-app-rtty-remote/files/etc/init.d/rtty-remote b/package/secubox/secubox-app-rtty-remote/files/etc/init.d/rtty-remote new file mode 100644 index 00000000..a73e59ab --- /dev/null +++ b/package/secubox/secubox-app-rtty-remote/files/etc/init.d/rtty-remote @@ -0,0 +1,32 @@ +#!/bin/sh /etc/rc.common + +START=95 +STOP=10 +USE_PROCD=1 + +PROG=/usr/sbin/rttyctl +CONF=/etc/config/rtty-remote + +start_service() { + local enabled + config_load rtty-remote + config_get enabled main enabled '0' + + [ "$enabled" = "1" ] || return 0 + + procd_open_instance rtty-remote + procd_set_param command $PROG server-daemon + procd_set_param respawn + procd_set_param stderr 1 + procd_set_param stdout 1 + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "rtty-remote" +} + +reload_service() { + stop + start +} diff --git a/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-auth.sh b/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-auth.sh new file mode 100644 index 00000000..8c0e4144 --- /dev/null +++ b/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-auth.sh @@ -0,0 +1,130 @@ +# rtty-auth.sh - Authentication Library for RTTY Remote +# +# Integration with master-link for secure node authentication +# + +AUTH_CACHE_DIR="/tmp/rtty-remote/auth" + +# Initialize auth cache +auth_init() { + mkdir -p "$AUTH_CACHE_DIR" 2>/dev/null +} + +# Get or create session token for remote node +# $1 = node address +auth_get_token() { + local addr="$1" + local cache_file="$AUTH_CACHE_DIR/token_${addr//[.:]/_}" + local ttl=3600 + + # Check cache + if [ -f "$cache_file" ]; then + local data=$(cat "$cache_file") + local ts=$(echo "$data" | cut -d: -f1) + local token=$(echo "$data" | cut -d: -f2-) + local now=$(date +%s) + + if [ $((now - ts)) -lt $ttl ]; then + echo "$token" + return 0 + fi + fi + + # Try to authenticate + local token=$(auth_login "$addr") + + if [ -n "$token" ]; then + echo "$(date +%s):$token" > "$cache_file" + echo "$token" + return 0 + fi + + # Fallback to anonymous + echo "00000000000000000000000000000000" +} + +# Authenticate to remote node +# $1 = node address +auth_login() { + local addr="$1" + + # Check if master-link is available + if [ -f /usr/lib/secubox/master-link.sh ]; then + . /usr/lib/secubox/master-link.sh + + # Get peer info + local peer_info=$(ml_peer_list 2>/dev/null | jsonfilter -e "@.peers[?(@.address=='$addr')]" 2>/dev/null) + + if [ -n "$peer_info" ]; then + local status=$(echo "$peer_info" | jsonfilter -e '@.status') + if [ "$status" = "approved" ]; then + # Use master-link token + local ml_token=$(ml_token_generate 2>/dev/null | jsonfilter -e '@.token') + if [ -n "$ml_token" ]; then + # Exchange master-link token for session + auth_exchange_ml_token "$addr" "$ml_token" + return $? + fi + fi + fi + fi + + # Try password-less login + local request=$(cat << EOF +{ + "jsonrpc": "2.0", + "id": 1, + "method": "call", + "params": ["00000000000000000000000000000000", "session", "login", {"username": "root", "password": ""}] +} +EOF +) + + local response=$(curl -s -m 10 \ + -H "Content-Type: application/json" \ + -d "$request" \ + "http://${addr}/ubus" 2>/dev/null) + + local token=$(echo "$response" | jsonfilter -e '@.result[1].ubus_rpc_session' 2>/dev/null) + + if [ -n "$token" ] && [ "$token" != "null" ]; then + echo "$token" + return 0 + fi + + return 1 +} + +# Exchange master-link token for session +# $1 = node address +# $2 = master-link token +auth_exchange_ml_token() { + local addr="$1" + local ml_token="$2" + + # TODO: Implement master-link token exchange endpoint on remote + # For now, use standard login + auth_login "$addr" +} + +# Revoke authentication for node +# $1 = node address +auth_revoke() { + local addr="$1" + rm -f "$AUTH_CACHE_DIR/token_${addr//[.:]/_}" 2>/dev/null +} + +# Clear all auth cache +auth_clear_all() { + rm -rf "$AUTH_CACHE_DIR"/* 2>/dev/null +} + +# Check if node is authenticated +# $1 = node address +auth_is_authenticated() { + local addr="$1" + local cache_file="$AUTH_CACHE_DIR/token_${addr//[.:]/_}" + + [ -f "$cache_file" ] && return 0 + return 1 +} diff --git a/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-proxy.sh b/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-proxy.sh new file mode 100644 index 00000000..d7334d5e --- /dev/null +++ b/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-proxy.sh @@ -0,0 +1,114 @@ +# rtty-proxy.sh - RPCD Proxy Library for RTTY Remote +# +# Functions for proxying RPCD calls to remote SecuBox nodes +# + +PROXY_CACHE_DIR="/tmp/rtty-remote/cache" + +# Initialize proxy cache +proxy_init() { + mkdir -p "$PROXY_CACHE_DIR" 2>/dev/null +} + +# Execute remote RPCD call +# $1 = node address +# $2 = ubus object +# $3 = method +# $4 = params (JSON) +# $5 = auth token +proxy_rpc_call() { + local addr="$1" + local object="$2" + local method="$3" + local params="${4:-{}}" + local token="${5:-00000000000000000000000000000000}" + + local rpc_id=$(date +%s%N | cut -c1-13) + + # Build JSON-RPC 2.0 request + local request=$(cat << EOF +{ + "jsonrpc": "2.0", + "id": $rpc_id, + "method": "call", + "params": ["$token", "$object", "$method", $params] +} +EOF +) + + # Execute request + curl -s -m 30 \ + -H "Content-Type: application/json" \ + -d "$request" \ + "http://${addr}/ubus" +} + +# List remote RPCD objects (with caching) +# $1 = node address +# $2 = auth token +proxy_list_objects() { + local addr="$1" + local token="${2:-00000000000000000000000000000000}" + + local cache_file="$PROXY_CACHE_DIR/objects_${addr//[.:]/_}" + local cache_ttl=60 + + # Check cache + if [ -f "$cache_file" ]; then + local mtime=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0) + local now=$(date +%s) + if [ $((now - mtime)) -lt $cache_ttl ]; then + cat "$cache_file" + return 0 + fi + fi + + # Fetch fresh list + local request=$(cat << EOF +{ + "jsonrpc": "2.0", + "id": 1, + "method": "list", + "params": ["$token", "*"] +} +EOF +) + + local response=$(curl -s -m 30 \ + -H "Content-Type: application/json" \ + -d "$request" \ + "http://${addr}/ubus") + + # Cache response + echo "$response" > "$cache_file" + echo "$response" +} + +# Batch execute multiple RPCD calls +# $1 = node address +# $2 = auth token +# $3 = batch JSON array +proxy_batch_call() { + local addr="$1" + local token="$2" + local batch="$3" + + local results="[]" + local count=0 + + echo "$batch" | jsonfilter -e '@[*]' 2>/dev/null | while read call; do + local object=$(echo "$call" | jsonfilter -e '@.object') + local method=$(echo "$call" | jsonfilter -e '@.method') + local params=$(echo "$call" | jsonfilter -e '@.params') + + local result=$(proxy_rpc_call "$addr" "$object" "$method" "$params" "$token") + count=$((count + 1)) + + echo "$result" + done +} + +# Clear proxy cache +proxy_clear_cache() { + rm -rf "$PROXY_CACHE_DIR"/* 2>/dev/null +} diff --git a/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-session.sh b/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-session.sh new file mode 100644 index 00000000..deb05983 --- /dev/null +++ b/package/secubox/secubox-app-rtty-remote/files/usr/lib/secubox/rtty-session.sh @@ -0,0 +1,194 @@ +# rtty-session.sh - Session Management Library for RTTY Remote +# +# SQLite-based session storage and replay functionality +# + +SESSION_DB="${SESSION_DB:-/srv/rtty-remote/sessions.db}" + +# Initialize session database +session_init_db() { + mkdir -p "$(dirname "$SESSION_DB")" 2>/dev/null + + sqlite3 "$SESSION_DB" << 'EOF' +CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + node_id TEXT NOT NULL, + node_address TEXT, + type TEXT NOT NULL, -- 'terminal', 'rpc', 'replay' + started_at INTEGER NOT NULL, + ended_at INTEGER, + duration INTEGER, + bytes_sent INTEGER DEFAULT 0, + bytes_recv INTEGER DEFAULT 0, + commands TEXT, -- JSON array of commands/calls + label TEXT, + metadata TEXT -- JSON metadata +); + +CREATE TABLE IF NOT EXISTS rpc_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER, + object TEXT NOT NULL, + method TEXT NOT NULL, + params TEXT, + result TEXT, + status_code INTEGER, + executed_at INTEGER NOT NULL, + duration_ms INTEGER, + FOREIGN KEY(session_id) REFERENCES sessions(id) +); + +CREATE INDEX IF NOT EXISTS idx_sessions_node ON sessions(node_id); +CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at); +CREATE INDEX IF NOT EXISTS idx_rpc_log_session ON rpc_log(session_id); +EOF +} + +# Start a new session +# $1 = node_id +# $2 = node_address +# $3 = type (terminal|rpc|replay) +session_start() { + local node_id="$1" + local node_address="$2" + local type="${3:-rpc}" + local now=$(date +%s) + + session_init_db + + sqlite3 "$SESSION_DB" "INSERT INTO sessions (node_id, node_address, type, started_at) VALUES ('$node_id', '$node_address', '$type', $now)" + + # Return session ID + sqlite3 "$SESSION_DB" "SELECT last_insert_rowid()" +} + +# End a session +# $1 = session_id +session_end() { + local session_id="$1" + local now=$(date +%s) + + sqlite3 "$SESSION_DB" "UPDATE sessions SET ended_at = $now, duration = $now - started_at WHERE id = $session_id" +} + +# Log an RPC call +# $1 = session_id +# $2 = object +# $3 = method +# $4 = params (JSON) +# $5 = result (JSON) +# $6 = status_code +# $7 = duration_ms +session_log_rpc() { + local session_id="$1" + local object="$2" + local method="$3" + local params="$4" + local result="$5" + local status_code="${6:-0}" + local duration_ms="${7:-0}" + local now=$(date +%s) + + # Escape single quotes + params=$(echo "$params" | sed "s/'/''/g") + result=$(echo "$result" | sed "s/'/''/g") + + sqlite3 "$SESSION_DB" "INSERT INTO rpc_log (session_id, object, method, params, result, status_code, executed_at, duration_ms) VALUES ($session_id, '$object', '$method', '$params', '$result', $status_code, $now, $duration_ms)" +} + +# List sessions +# $1 = node_id (optional filter) +# $2 = limit (default 50) +session_list() { + local node_id="$1" + local limit="${2:-50}" + + session_init_db + + local where="" + [ -n "$node_id" ] && where="WHERE node_id = '$node_id'" + + sqlite3 -json "$SESSION_DB" "SELECT id, node_id, node_address, type, datetime(started_at, 'unixepoch') as started, duration, label FROM sessions $where ORDER BY started_at DESC LIMIT $limit" +} + +# Get session details +# $1 = session_id +session_get() { + local session_id="$1" + + session_init_db + + sqlite3 -json "$SESSION_DB" "SELECT * FROM sessions WHERE id = $session_id" +} + +# Get RPC log for session +# $1 = session_id +session_get_rpc_log() { + local session_id="$1" + + sqlite3 -json "$SESSION_DB" "SELECT * FROM rpc_log WHERE session_id = $session_id ORDER BY executed_at" +} + +# Label a session +# $1 = session_id +# $2 = label +session_label() { + local session_id="$1" + local label="$2" + + sqlite3 "$SESSION_DB" "UPDATE sessions SET label = '$label' WHERE id = $session_id" +} + +# Delete a session +# $1 = session_id +session_delete() { + local session_id="$1" + + sqlite3 "$SESSION_DB" "DELETE FROM rpc_log WHERE session_id = $session_id" + sqlite3 "$SESSION_DB" "DELETE FROM sessions WHERE id = $session_id" +} + +# Export session to JSON +# $1 = session_id +session_export() { + local session_id="$1" + + local session=$(session_get "$session_id") + local rpc_log=$(session_get_rpc_log "$session_id") + + cat << EOF +{ + "session": $session, + "rpc_log": $rpc_log +} +EOF +} + +# Cleanup old sessions +# $1 = days (default 7) +session_cleanup() { + local days="${1:-7}" + local cutoff=$(($(date +%s) - (days * 86400))) + + sqlite3 "$SESSION_DB" "DELETE FROM rpc_log WHERE session_id IN (SELECT id FROM sessions WHERE ended_at < $cutoff AND label IS NULL)" + sqlite3 "$SESSION_DB" "DELETE FROM sessions WHERE ended_at < $cutoff AND label IS NULL" +} + +# Get session statistics +session_stats() { + session_init_db + + local total=$(sqlite3 "$SESSION_DB" "SELECT COUNT(*) FROM sessions") + local active=$(sqlite3 "$SESSION_DB" "SELECT COUNT(*) FROM sessions WHERE ended_at IS NULL") + local rpc_calls=$(sqlite3 "$SESSION_DB" "SELECT COUNT(*) FROM rpc_log") + local nodes=$(sqlite3 "$SESSION_DB" "SELECT COUNT(DISTINCT node_id) FROM sessions") + + cat << EOF +{ + "total_sessions": $total, + "active_sessions": $active, + "total_rpc_calls": $rpc_calls, + "unique_nodes": $nodes +} +EOF +} diff --git a/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl b/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl new file mode 100644 index 00000000..6a61b2e8 --- /dev/null +++ b/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl @@ -0,0 +1,642 @@ +#!/bin/sh +# +# rttyctl - SecuBox RTTY Remote Control CLI +# +# Remote control assistant for SecuBox mesh nodes +# Provides RPCD proxy, terminal access, and session replay +# + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +SCRIPT_NAME="rttyctl" +VERSION="0.1.0" + +# Configuration +CONFIG_FILE="/etc/config/rtty-remote" +SESSION_DB="/srv/rtty-remote/sessions.db" +CACHE_DIR="/tmp/rtty-remote" +LOG_FILE="/var/log/rtty-remote.log" + +# Load libraries +[ -f /usr/lib/secubox/rtty-proxy.sh ] && . /usr/lib/secubox/rtty-proxy.sh +[ -f /usr/lib/secubox/rtty-session.sh ] && . /usr/lib/secubox/rtty-session.sh +[ -f /usr/lib/secubox/rtty-auth.sh ] && . /usr/lib/secubox/rtty-auth.sh + +# Load master-link if available +[ -f /usr/lib/secubox/master-link.sh ] && . /usr/lib/secubox/master-link.sh + +#------------------------------------------------------------------------------ +# Utilities +#------------------------------------------------------------------------------ + +log() { + local level="$1" + shift + echo "[$level] $*" >> "$LOG_FILE" + [ "$level" = "error" ] && echo "Error: $*" >&2 +} + +die() { + echo "Error: $*" >&2 + exit 1 +} + +ensure_dirs() { + mkdir -p "$CACHE_DIR" 2>/dev/null + mkdir -p "$(dirname "$SESSION_DB")" 2>/dev/null +} + +get_config() { + local section="$1" + local option="$2" + local default="$3" + + config_load rtty-remote + config_get value "$section" "$option" "$default" + echo "$value" +} + +#------------------------------------------------------------------------------ +# Node Management +#------------------------------------------------------------------------------ + +cmd_nodes() { + echo "SecuBox RTTY Remote - Mesh Nodes" + echo "================================" + echo "" + + # Try to get peers from master-link first + if command -v ml_peer_list >/dev/null 2>&1; then + local peers=$(ml_peer_list 2>/dev/null) + if [ -n "$peers" ]; then + echo "From Master-Link:" + echo "$peers" | jsonfilter -e '@.peers[*]' 2>/dev/null | while read peer; do + local fp=$(echo "$peer" | jsonfilter -e '@.fingerprint') + local name=$(echo "$peer" | jsonfilter -e '@.name') + local status=$(echo "$peer" | jsonfilter -e '@.status') + local addr=$(echo "$peer" | jsonfilter -e '@.address') + printf " %-12s %-20s %-15s %s\n" "$fp" "$name" "$addr" "$status" + done + echo "" + fi + fi + + # Try secubox-p2p peers + if [ -x /usr/sbin/secubox-p2p ]; then + echo "From P2P Mesh:" + /usr/sbin/secubox-p2p peers 2>/dev/null | head -20 + fi + + # Also check WireGuard peers + if command -v wg >/dev/null 2>&1; then + echo "" + echo "WireGuard Peers:" + wg show all peers 2>/dev/null | while read iface peer; do + local endpoint=$(wg show "$iface" endpoints 2>/dev/null | grep "$peer" | awk '{print $2}') + local handshake=$(wg show "$iface" latest-handshakes 2>/dev/null | grep "$peer" | awk '{print $2}') + [ -n "$endpoint" ] && printf " %-15s %s (handshake: %ss ago)\n" "$endpoint" "${peer:0:12}..." "$handshake" + done + fi +} + +cmd_node() { + local node_id="$1" + [ -z "$node_id" ] && die "Usage: rttyctl node " + + echo "Node: $node_id" + echo "==============" + + # Check if in master-link + if command -v ml_peer_list >/dev/null 2>&1; then + local peer_info=$(ml_peer_list 2>/dev/null | jsonfilter -e "@.peers[?(@.fingerprint=='$node_id')]" 2>/dev/null) + if [ -n "$peer_info" ]; then + echo "Master-Link Status:" + echo " Name: $(echo "$peer_info" | jsonfilter -e '@.name')" + echo " Status: $(echo "$peer_info" | jsonfilter -e '@.status')" + echo " Address: $(echo "$peer_info" | jsonfilter -e '@.address')" + echo " Role: $(echo "$peer_info" | jsonfilter -e '@.role')" + fi + fi + + # Try to ping + local addr=$(get_node_address "$node_id") + if [ -n "$addr" ]; then + echo "" + echo "Connectivity:" + if ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then + echo " Ping: OK" + else + echo " Ping: FAILED" + fi + fi +} + +get_node_address() { + local node_id="$1" + + # Check UCI for known nodes + config_load rtty-remote + local addr="" + config_get addr "node_${node_id}" address "" + [ -n "$addr" ] && { echo "$addr"; return; } + + # Check master-link + if command -v ml_peer_list >/dev/null 2>&1; then + addr=$(ml_peer_list 2>/dev/null | jsonfilter -e "@.peers[?(@.fingerprint=='$node_id')].address" 2>/dev/null) + [ -n "$addr" ] && { echo "$addr"; return; } + fi + + # Fallback: assume node_id is an IP + echo "$node_id" +} + +#------------------------------------------------------------------------------ +# RPCD Proxy - Core Feature +#------------------------------------------------------------------------------ + +cmd_rpc() { + local node_id="$1" + local object="$2" + local method="$3" + shift 3 + local params="$*" + + [ -z "$node_id" ] || [ -z "$object" ] || [ -z "$method" ] && { + echo "Usage: rttyctl rpc [params_json]" + echo "" + echo "Examples:" + echo " rttyctl rpc 10.100.0.2 luci.system-hub status" + echo " rttyctl rpc sb-01 luci.haproxy vhost_list" + echo " rttyctl rpc 192.168.255.2 system board" + exit 1 + } + + local addr=$(get_node_address "$node_id") + [ -z "$addr" ] && die "Cannot resolve node address for: $node_id" + + # Get authentication token + local auth_token=$(get_auth_token "$addr") + + # Build JSON-RPC request + local rpc_id=$(date +%s%N | cut -c1-13) + + if [ -z "$params" ] || [ "$params" = "{}" ]; then + params="{}" + fi + + # Build request manually for correct format (jshn doesn't handle nested objects in arrays well) + local request=$(cat << EOF +{"jsonrpc":"2.0","id":$rpc_id,"method":"call","params":["$auth_token","$object","$method",$params]} +EOF +) + + # Make HTTP request to remote ubus + local timeout=$(get_config proxy rpc_timeout 30) + local http_port=$(get_config proxy http_port 8081) + local url="http://${addr}:${http_port}/ubus" + + log "info" "RPC call to $addr: $object.$method" + + local response=$(curl -s -m "$timeout" \ + -H "Content-Type: application/json" \ + -d "$request" \ + "$url" 2>&1) + + local curl_rc=$? + + if [ $curl_rc -ne 0 ]; then + die "Connection failed to $addr (curl error: $curl_rc)" + fi + + # Check for JSON-RPC error + local error=$(echo "$response" | jsonfilter -e '@.error.message' 2>/dev/null) + if [ -n "$error" ]; then + die "RPC error: $error" + fi + + # Extract and display result + local result=$(echo "$response" | jsonfilter -e '@.result[1]' 2>/dev/null) + if [ -z "$result" ]; then + result=$(echo "$response" | jsonfilter -e '@.result' 2>/dev/null) + fi + + echo "$result" +} + +cmd_rpc_list() { + local node_id="$1" + [ -z "$node_id" ] && die "Usage: rttyctl rpc-list " + + local addr=$(get_node_address "$node_id") + [ -z "$addr" ] && die "Cannot resolve node address for: $node_id" + + echo "RPCD Objects on $node_id ($addr)" + echo "=================================" + + # Get session first + local auth_token=$(get_auth_token "$addr") + + # List ubus objects + json_init + json_add_string "jsonrpc" "2.0" + json_add_int "id" 1 + json_add_string "method" "list" + json_add_array "params" + json_add_string "" "$auth_token" + json_add_string "" "*" + json_close_array + local request=$(json_dump) + + local http_port=$(get_config proxy http_port 8081) + local response=$(curl -s -m 30 \ + -H "Content-Type: application/json" \ + -d "$request" \ + "http://${addr}:${http_port}/ubus" 2>&1) + + # Parse and display objects + echo "$response" | jsonfilter -e '@.result[1]' 2>/dev/null | \ + python3 -c "import sys,json; d=json.load(sys.stdin); [print(f' {k}') for k in sorted(d.keys())]" 2>/dev/null || \ + echo "$response" +} + +cmd_rpc_batch() { + local node_id="$1" + local batch_file="$2" + + [ -z "$node_id" ] || [ -z "$batch_file" ] && die "Usage: rttyctl rpc-batch " + [ ! -f "$batch_file" ] && die "Batch file not found: $batch_file" + + echo "Executing batch RPC to $node_id..." + + local count=0 + while read -r line; do + [ -z "$line" ] && continue + local object=$(echo "$line" | jsonfilter -e '@.object') + local method=$(echo "$line" | jsonfilter -e '@.method') + local params=$(echo "$line" | jsonfilter -e '@.params') + + echo "[$count] $object.$method" + cmd_rpc "$node_id" "$object" "$method" "$params" + echo "" + count=$((count + 1)) + done < "$batch_file" + + echo "Executed $count calls" +} + +#------------------------------------------------------------------------------ +# Authentication +#------------------------------------------------------------------------------ + +get_auth_token() { + local addr="$1" + local cache_file="$CACHE_DIR/auth_${addr//[.:]/_}" + + # Check cache + if [ -f "$cache_file" ]; then + local cached=$(cat "$cache_file") + local ts=$(echo "$cached" | cut -d: -f1) + local token=$(echo "$cached" | cut -d: -f2-) + local now=$(date +%s) + local ttl=$(get_config main session_ttl 3600) + + if [ $((now - ts)) -lt $ttl ]; then + echo "$token" + return 0 + fi + fi + + # Get new session via login + # For now, use anonymous session (00000000000000000000000000000000) + # TODO: Implement proper master-link authentication + local anon_token="00000000000000000000000000000000" + + # Try to get authenticated session + json_init + json_add_string "jsonrpc" "2.0" + json_add_int "id" 1 + json_add_string "method" "call" + json_add_array "params" + json_add_string "" "$anon_token" + json_add_string "" "session" + json_add_string "" "login" + json_add_object "" + json_add_string "username" "root" + json_add_string "password" "" + json_close_object + json_close_array + local request=$(json_dump) + + local http_port=$(get_config proxy http_port 8081) + local response=$(curl -s -m 10 \ + -H "Content-Type: application/json" \ + -d "$request" \ + "http://${addr}:${http_port}/ubus" 2>&1) + + local token=$(echo "$response" | jsonfilter -e '@.result[1].ubus_rpc_session' 2>/dev/null) + + if [ -n "$token" ] && [ "$token" != "null" ]; then + echo "$(date +%s):$token" > "$cache_file" + echo "$token" + else + # Fallback to anonymous + echo "$anon_token" + fi +} + +cmd_auth() { + local node_id="$1" + [ -z "$node_id" ] && die "Usage: rttyctl auth " + + local addr=$(get_node_address "$node_id") + echo "Authenticating to $node_id ($addr)..." + + # Clear cache to force re-auth + rm -f "$CACHE_DIR/auth_${addr//[.:]/_}" 2>/dev/null + + local token=$(get_auth_token "$addr") + if [ -n "$token" ] && [ "$token" != "00000000000000000000000000000000" ]; then + echo "Authenticated successfully" + echo "Session: ${token:0:16}..." + else + echo "Using anonymous session (limited access)" + fi +} + +cmd_revoke() { + local node_id="$1" + [ -z "$node_id" ] && die "Usage: rttyctl revoke " + + local addr=$(get_node_address "$node_id") + rm -f "$CACHE_DIR/auth_${addr//[.:]/_}" 2>/dev/null + echo "Revoked authentication for $node_id" +} + +#------------------------------------------------------------------------------ +# Terminal/Connection (Placeholder for RTTY) +#------------------------------------------------------------------------------ + +cmd_connect() { + local node_id="$1" + [ -z "$node_id" ] && die "Usage: rttyctl connect " + + local addr=$(get_node_address "$node_id") + + echo "Connecting to $node_id ($addr)..." + echo "" + echo "Note: Full terminal support requires RTTY package." + echo "For now, use SSH:" + echo " ssh root@$addr" + echo "" + echo "Or use RPCD proxy:" + echo " rttyctl rpc $node_id system board" +} + +cmd_disconnect() { + local session_id="$1" + [ -z "$session_id" ] && die "Usage: rttyctl disconnect " + echo "Session $session_id disconnected" +} + +#------------------------------------------------------------------------------ +# Session Management (Placeholder) +#------------------------------------------------------------------------------ + +cmd_sessions() { + local node_id="$1" + + echo "Active Sessions" + echo "===============" + + if [ ! -f "$SESSION_DB" ]; then + echo "No sessions recorded" + return + fi + + sqlite3 "$SESSION_DB" "SELECT id, node_id, type, started_at FROM sessions ORDER BY started_at DESC LIMIT 20" 2>/dev/null || \ + echo "Session database not initialized" +} + +cmd_replay() { + local session_id="$1" + local target_node="$2" + + [ -z "$session_id" ] || [ -z "$target_node" ] && die "Usage: rttyctl replay " + + echo "Replaying session $session_id to $target_node..." + echo "Note: Session replay requires avatar-tap integration (coming soon)" +} + +cmd_export() { + local session_id="$1" + [ -z "$session_id" ] && die "Usage: rttyctl export " + + echo "Exporting session $session_id..." + echo "Note: Session export requires implementation" +} + +#------------------------------------------------------------------------------ +# Server Management +#------------------------------------------------------------------------------ + +cmd_server() { + local action="$1" + + case "$action" in + start) + /etc/init.d/rtty-remote start + echo "RTTY Remote server started" + ;; + stop) + /etc/init.d/rtty-remote stop + echo "RTTY Remote server stopped" + ;; + status) + echo "RTTY Remote Server Status" + echo "=========================" + + local enabled=$(get_config main enabled 0) + local port=$(get_config main server_port 7681) + + echo "Enabled: $enabled" + echo "Port: $port" + + if pgrep -f "rttyctl server-daemon" >/dev/null 2>&1; then + echo "Running: yes" + else + echo "Running: no" + fi + ;; + *) + echo "Usage: rttyctl server start|stop|status" + ;; + esac +} + +cmd_server_daemon() { + # Internal: daemon mode for procd + ensure_dirs + log "info" "RTTY Remote daemon starting..." + + # TODO: Implement HTTP server for incoming RPC proxy requests + # For now, just keep the process alive + while true; do + sleep 60 + done +} + +#------------------------------------------------------------------------------ +# JSON Output (for RPCD) +#------------------------------------------------------------------------------ + +cmd_json_status() { + json_init + json_add_boolean "enabled" "$(get_config main enabled 0)" + json_add_int "port" "$(get_config main server_port 7681)" + + if pgrep -f "rttyctl server-daemon" >/dev/null 2>&1; then + json_add_boolean "running" 1 + else + json_add_boolean "running" 0 + fi + + json_add_int "active_sessions" 0 + json_add_int "total_nodes" 0 + + json_dump +} + +cmd_json_nodes() { + json_init + json_add_array "nodes" + + # Add nodes from master-link + if command -v ml_peer_list >/dev/null 2>&1; then + ml_peer_list 2>/dev/null | jsonfilter -e '@.peers[*]' 2>/dev/null | while read peer; do + json_add_object "" + json_add_string "id" "$(echo "$peer" | jsonfilter -e '@.fingerprint')" + json_add_string "name" "$(echo "$peer" | jsonfilter -e '@.name')" + json_add_string "address" "$(echo "$peer" | jsonfilter -e '@.address')" + json_add_string "status" "$(echo "$peer" | jsonfilter -e '@.status')" + json_close_object + done + fi + + json_close_array + json_dump +} + +#------------------------------------------------------------------------------ +# Help +#------------------------------------------------------------------------------ + +show_help() { + cat << 'EOF' +rttyctl - SecuBox RTTY Remote Control + +Usage: rttyctl [options] + +Node Management: + nodes List all mesh nodes with status + node Show detailed node info + connect Start terminal session to node + disconnect End terminal session + +RPCD Proxy: + rpc [params] Execute remote RPCD call + rpc-list List available RPCD objects + rpc-batch Execute batch RPCD calls + +Session Management: + sessions [node_id] List active/recent sessions + replay Replay captured session to node + export Export session as JSON + +Server Control: + server start Start local RTTY server + server stop Stop local RTTY server + server status Show server status + +Authentication: + auth Authenticate to remote node + revoke Revoke authentication + +JSON Output (for RPCD): + json-status Status as JSON + json-nodes Nodes as JSON + +Examples: + rttyctl nodes + rttyctl rpc 10.100.0.2 luci.system-hub status + rttyctl rpc sb-01 luci.haproxy vhost_list + rttyctl rpc-list 192.168.255.2 + +Version: $VERSION +EOF +} + +#------------------------------------------------------------------------------ +# Main +#------------------------------------------------------------------------------ + +ensure_dirs + +case "$1" in + nodes) + cmd_nodes + ;; + node) + cmd_node "$2" + ;; + connect) + cmd_connect "$2" + ;; + disconnect) + cmd_disconnect "$2" + ;; + rpc) + shift + cmd_rpc "$@" + ;; + rpc-list) + cmd_rpc_list "$2" + ;; + rpc-batch) + cmd_rpc_batch "$2" "$3" + ;; + sessions) + cmd_sessions "$2" + ;; + replay) + cmd_replay "$2" "$3" + ;; + export) + cmd_export "$2" + ;; + server) + cmd_server "$2" + ;; + server-daemon) + cmd_server_daemon + ;; + auth) + cmd_auth "$2" + ;; + revoke) + cmd_revoke "$2" + ;; + json-status) + cmd_json_status + ;; + json-nodes) + cmd_json_nodes + ;; + -h|--help|help) + show_help + ;; + *) + show_help + exit 1 + ;; +esac