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)
|
# 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
|
> **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches
|
||||||
|
|
||||||
@ -10,6 +10,21 @@ _Last updated: 2026-03-08 (Maegia Domains Fix)_
|
|||||||
|
|
||||||
### 2026-03-08
|
### 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**
|
- **Vortex DNS Zone Management & Secondary DNS**
|
||||||
- Added zone commands: `vortexctl zone list/dump/import/export/reload`
|
- Added zone commands: `vortexctl zone list/dump/import/export/reload`
|
||||||
- Added secondary DNS commands: `vortexctl secondary list/add/remove`
|
- Added secondary DNS commands: `vortexctl secondary list/add/remove`
|
||||||
|
|||||||
@ -515,7 +515,9 @@
|
|||||||
"Bash(./scripts/check-sbom-prereqs.sh:*)",
|
"Bash(./scripts/check-sbom-prereqs.sh:*)",
|
||||||
"Bash(git revert:*)",
|
"Bash(git revert:*)",
|
||||||
"Bash(__NEW_LINE_17d05a792c15c52e__ echo \"\")",
|
"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