feat(rtty-remote): Add RTTY Remote Control module with support panel
New packages: - secubox-app-rtty-remote: Backend with rttyctl CLI - luci-app-rtty-remote: LuCI dashboard with KISS theme Features: - RPCD Proxy: Execute remote ubus calls to mesh nodes over HTTP - Support Panel: Remote assistance with shareable session codes - Session tracking: SQLite database for audit trail - Quick actions: System info, network, services, vhosts, reboot - RPC Console: Execute arbitrary ubus commands CLI commands: - rttyctl nodes - List mesh nodes - rttyctl rpc <node> <object> <method> - Execute remote RPCD - rttyctl rpc-list <node> - List available objects - rttyctl sessions - Show session history LuCI views: - dashboard.js: Node management, stats, RPC console - support.js: Remote assistance with session codes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
68c9449c01
commit
2c763c3583
@ -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`
|
||||
|
||||
@ -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: \")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
16
package/secubox/luci-app-rtty-remote/Makefile
Normal file
16
package/secubox/luci-app-rtty-remote/Makefile
Normal file
@ -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))
|
||||
@ -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)
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
44
package/secubox/secubox-app-rtty-remote/Makefile
Normal file
44
package/secubox/secubox-app-rtty-remote/Makefile
Normal file
@ -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))
|
||||
@ -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'
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
642
package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl
Normal file
642
package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl
Normal file
@ -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 <node_id>"
|
||||
|
||||
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 <node_id> <object> <method> [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 <node_id>"
|
||||
|
||||
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 <node_id> <file.json>"
|
||||
[ ! -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 <node_id>"
|
||||
|
||||
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 <node_id>"
|
||||
|
||||
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 <node_id>"
|
||||
|
||||
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 <session_id>"
|
||||
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 <session_id> <target_node>"
|
||||
|
||||
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 <session_id>"
|
||||
|
||||
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 <command> [options]
|
||||
|
||||
Node Management:
|
||||
nodes List all mesh nodes with status
|
||||
node <id> Show detailed node info
|
||||
connect <node_id> Start terminal session to node
|
||||
disconnect <session_id> End terminal session
|
||||
|
||||
RPCD Proxy:
|
||||
rpc <node> <object> <method> [params] Execute remote RPCD call
|
||||
rpc-list <node> List available RPCD objects
|
||||
rpc-batch <node> <file.json> Execute batch RPCD calls
|
||||
|
||||
Session Management:
|
||||
sessions [node_id] List active/recent sessions
|
||||
replay <session_id> <node> Replay captured session to node
|
||||
export <session_id> 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 <node_id> Authenticate to remote node
|
||||
revoke <node_id> 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
|
||||
Loading…
Reference in New Issue
Block a user