diff --git a/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/support.js b/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/support.js index f1eb8fa2..12b06333 100644 --- a/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/support.js +++ b/package/secubox/luci-app-rtty-remote/htdocs/luci-static/resources/view/rtty-remote/support.js @@ -32,37 +32,76 @@ var callRpcCall = rpc.declare({ expect: {} }); -// Support session management -var supportSessions = {}; +// Token-based shared access +var callTokenGenerate = rpc.declare({ + object: 'luci.rtty-remote', + method: 'token_generate', + params: ['ttl', 'permissions'], + expect: {} +}); + +var callTokenValidate = rpc.declare({ + object: 'luci.rtty-remote', + method: 'token_validate', + params: ['code'], + expect: {} +}); + +var callTokenList = rpc.declare({ + object: 'luci.rtty-remote', + method: 'token_list', + expect: {} +}); + +var callTokenRevoke = rpc.declare({ + object: 'luci.rtty-remote', + method: 'token_revoke', + params: ['code'], + expect: {} +}); + +var callTokenRpc = rpc.declare({ + object: 'luci.rtty-remote', + method: 'token_rpc', + params: ['code', 'object', 'method', 'params'], + expect: {} +}); return view.extend({ handleSaveApply: null, handleSave: null, handleReset: null, + currentToken: null, + currentNode: null, + isProvider: false, // true if we're providing support (connected via token) + load: function() { return Promise.all([ callStatus(), - callGetNodes() + callGetNodes(), + callTokenList() ]); }, render: function(data) { var status = data[0] || {}; var nodesData = data[1] || {}; + var tokenData = data[2] || {}; var nodes = nodesData.nodes || []; + var tokens = tokenData.tokens || []; var view = E('div', { 'class': 'cbi-map' }, [ this.renderHeader(), - this.renderSupportCodeSection(), - this.renderActiveSessionsSection(), + this.renderSupportCodeSection(tokens), + this.renderActiveTokensSection(tokens), this.renderRemoteNodesSection(nodes), this.renderTerminalSection(), this.renderSharedControlsSection() ]); - // Start polling for session updates - poll.add(L.bind(this.pollSessions, this), 5); + // Start polling for token updates + poll.add(L.bind(this.pollTokens, this), 5); return view; }, @@ -71,20 +110,17 @@ return view.extend({ 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.') + 'Provide or receive remote assistance with token-based access. No authentication needed - just share the code.') ]); }, - renderSupportCodeSection: function() { + renderSupportCodeSection: function(tokens) { 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 + // Provide support section (connect TO someone else) 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.'), @@ -92,9 +128,10 @@ return view.extend({ E('input', { 'type': 'text', 'id': 'remote-support-code', - 'placeholder': 'Enter support code (e.g., SB-XXXX)', + 'placeholder': 'Enter code (e.g., ABC123)', 'class': 'cbi-input-text', - 'style': 'flex: 1; font-family: monospace; text-transform: uppercase;' + 'style': 'flex: 1; font-family: monospace; text-transform: uppercase; letter-spacing: 2px; font-size: 1.2em;', + 'maxlength': '6' }), E('button', { 'class': 'cbi-button cbi-button-positive', @@ -102,44 +139,96 @@ return view.extend({ }, 'Connect') ]) ]), - // Request support section + // Request support section (let someone connect TO me) E('div', {}, [ E('h4', {}, 'Request Support'), - E('p', { 'style': 'font-size: 0.9em; color: #666;' }, 'Share this code with your support technician.'), + E('p', { 'style': 'font-size: 0.9em; color: #666;' }, 'Generate a code and share it 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'), + 'style': 'font-size: 2em; padding: 0.5em 1em; background: #fff; border: 2px solid #4a9; border-radius: 8px; font-family: monospace; letter-spacing: 3px; color: #333;' + }, '------'), E('button', { 'class': 'cbi-button cbi-button-action', - 'click': L.bind(this.handleStartSupportSession, this, supportCode) - }, 'Start Session') + 'id': 'btn-generate-token', + 'click': L.bind(this.handleGenerateToken, this) + }, 'Generate Code') + ]), + E('div', { 'style': 'margin-top: 0.5em;' }, [ + E('label', { 'style': 'font-size: 0.9em;' }, 'Validity: '), + E('select', { 'id': 'token-ttl', 'class': 'cbi-input-select' }, [ + E('option', { 'value': '1800' }, '30 minutes'), + E('option', { 'value': '3600', 'selected': 'selected' }, '1 hour'), + E('option', { 'value': '7200' }, '2 hours'), + E('option', { 'value': '14400' }, '4 hours') + ]) ]) ]) ]) ]); }, - 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') + renderActiveTokensSection: function(tokens) { + var hasTokens = tokens && tokens.length > 0; + + return E('div', { 'class': 'cbi-section', 'id': 'active-tokens-section', 'style': hasTokens ? '' : 'display: none;' }, [ + E('h3', {}, 'Active Support Tokens'), + E('div', { 'id': 'active-tokens-list' }, [ + hasTokens ? this.renderTokensTable(tokens) : E('p', { 'style': 'color: #666;' }, 'No active tokens') ]) ]); }, + renderTokensTable: function(tokens) { + var self = this; + + var table = E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, 'Code'), + E('th', { 'class': 'th' }, 'Expires In'), + E('th', { 'class': 'th' }, 'Used'), + E('th', { 'class': 'th' }, 'Permissions'), + E('th', { 'class': 'th' }, 'Actions') + ]) + ]); + + tokens.forEach(function(token) { + var now = Math.floor(Date.now() / 1000); + var remaining = token.expires - now; + var mins = Math.floor(remaining / 60); + + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, E('code', { 'style': 'font-size: 1.2em; letter-spacing: 2px;' }, token.code)), + E('td', { 'class': 'td' }, mins > 0 ? mins + 'm' : 'expired'), + E('td', { 'class': 'td' }, String(token.used || 0)), + E('td', { 'class': 'td' }, token.permissions), + E('td', { 'class': 'td' }, [ + E('button', { + 'class': 'cbi-button', + 'click': function() { + navigator.clipboard.writeText(token.code).then(function() { + ui.addNotification(null, E('p', 'Code copied: ' + token.code), 'success'); + }); + } + }, 'Copy'), + E('button', { + 'class': 'cbi-button cbi-button-negative', + 'style': 'margin-left: 0.5em;', + 'click': L.bind(self.handleRevokeToken, self, token.code) + }, 'Revoke') + ]) + ])); + }); + + return table; + }, + renderRemoteNodesSection: function(nodes) { var self = this; if (nodes.length === 0) { return E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Quick Connect'), + E('h3', {}, 'Direct Connect'), E('p', { 'style': 'color: #666;' }, 'No mesh nodes available. Enter IP address manually:'), E('div', { 'style': 'display: flex; gap: 0.5em;' }, [ E('input', { @@ -169,7 +258,7 @@ return view.extend({ }); return E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, 'Quick Connect'), + E('h3', {}, 'Direct Connect'), E('div', { 'style': 'display: flex; flex-wrap: wrap;' }, nodeButtons) ]); }, @@ -182,14 +271,9 @@ return view.extend({ 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('span', { 'id': 'connection-mode', 'style': 'margin-right: 1em; padding: 0.25em 0.5em; border-radius: 4px; font-size: 0.9em;' }, ''), E('button', { 'class': 'cbi-button cbi-button-negative', - 'style': 'margin-left: 0.5em;', 'click': L.bind(this.handleDisconnect, this) }, 'Disconnect') ]) @@ -215,74 +299,100 @@ return view.extend({ 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('h3', {}, 'Quick Actions'), + E('p', { 'style': 'color: #666;' }, 'Common diagnostic commands.'), 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, 'network.interface', 'dump') }, 'Network'), + E('button', { 'class': 'cbi-button', 'click': L.bind(this.handleQuickAction, this, 'luci.system-hub', 'status') }, 'Services'), 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', 'click': L.bind(this.handleQuickAction, this, 'system', 'info') }, 'Memory'), 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; - }, + // Token generation + handleGenerateToken: function() { + var self = this; + var ttl = parseInt(document.getElementById('token-ttl').value) || 3600; - // Event handlers - handleCopyCode: function(code) { - navigator.clipboard.writeText(code).then(function() { - ui.addNotification(null, E('p', 'Support code copied to clipboard'), 'success'); + callTokenGenerate(ttl, 'rpc,terminal').then(function(result) { + if (result.success && result.code) { + document.getElementById('my-support-code').textContent = result.code; + document.getElementById('active-tokens-section').style.display = 'block'; + + ui.addNotification(null, E('p', [ + 'Support code generated: ', + E('strong', { 'style': 'font-size: 1.2em; letter-spacing: 2px;' }, result.code), + E('br'), + 'Valid for ' + Math.floor(ttl / 60) + ' minutes. Share this code with your support technician.' + ]), 'success'); + + // Refresh token list + self.pollTokens(); + } else { + ui.addNotification(null, E('p', 'Failed to generate token: ' + (result.error || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', 'Error: ' + err.message), 'error'); }); }, - 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'); - }, - + // Join session with token handleJoinSession: function() { + var self = this; 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'); + if (!code || code.length !== 6) { + ui.addNotification(null, E('p', 'Invalid code format. Enter 6-character code.'), 'error'); return; } - ui.addNotification(null, E('p', 'Connecting to support session: ' + code), 'info'); + ui.addNotification(null, E('p', 'Validating token: ' + 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' - }; + callTokenValidate(code).then(function(result) { + if (result.valid !== false && result.code) { + self.currentToken = code; + self.isProvider = true; - document.getElementById('active-sessions-section').style.display = 'block'; - this.updateSessionsList(); + // Show terminal and connect + document.getElementById('terminal-section').style.display = 'block'; + document.getElementById('shared-controls-section').style.display = 'block'; + document.getElementById('terminal-target').textContent = result.node_id + ' (' + result.node_ip + ')'; + document.getElementById('terminal-output').textContent = ''; + + var modeSpan = document.getElementById('connection-mode'); + modeSpan.textContent = 'TOKEN: ' + code; + modeSpan.style.background = '#4a9'; + modeSpan.style.color = '#fff'; + + self.currentNode = result.node_ip; + self.appendTerminal('Connected via token: ' + code + '\n'); + self.appendTerminal('Node: ' + result.node_id + ' (' + result.node_ip + ')\n'); + self.appendTerminal('Permissions: ' + result.permissions + '\n\n'); + self.appendTerminal('Type commands or use Quick Actions below.\n'); + + document.getElementById('terminal-input').focus(); + + ui.addNotification(null, E('p', 'Connected to ' + result.node_id + ' via token'), 'success'); + } else { + ui.addNotification(null, E('p', 'Invalid or expired token: ' + (result.error || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', 'Validation error: ' + err.message), 'error'); + }); + }, + + handleRevokeToken: function(code) { + var self = this; + + if (!confirm('Revoke token ' + code + '?')) return; + + callTokenRevoke(code).then(function(result) { + ui.addNotification(null, E('p', 'Token ' + code + ' revoked'), 'info'); + self.pollTokens(); + }); }, handleQuickConnect: function() { @@ -301,15 +411,20 @@ return view.extend({ connectToNode: function(address, name) { var self = this; + this.currentNode = address; + this.currentToken = null; + this.isProvider = false; + 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; + var modeSpan = document.getElementById('connection-mode'); + modeSpan.textContent = 'DIRECT'; + modeSpan.style.background = '#369'; + modeSpan.style.color = '#fff'; - // Test connection with system board call this.appendTerminal('Connecting to ' + address + '...\n'); callRpcCall(address, 'system', 'board', '{}').then(function(result) { @@ -327,30 +442,11 @@ return view.extend({ handleDisconnect: function() { this.currentNode = null; + this.currentToken = null; + this.isProvider = false; 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'); + ui.addNotification(null, E('p', 'Disconnected'), 'info'); }, handleTerminalKeydown: function(ev) { @@ -364,8 +460,8 @@ return view.extend({ this.appendTerminal('$ ' + cmd + '\n'); - if (!this.currentNode) { - this.appendTerminal('Error: Not connected to any node\n'); + if (!this.currentNode && !this.currentToken) { + this.appendTerminal('Error: Not connected\n'); return; } @@ -384,7 +480,21 @@ return view.extend({ var object = objMethod.slice(0, -1).join('.'); var method = objMethod[objMethod.length - 1]; - callRpcCall(this.currentNode, object, method, params).then(function(result) { + this.executeRpc(object, method, params); + }, + + executeRpc: function(object, method, params) { + var self = this; + + // Use token-based RPC if we connected via token + var rpcCall; + if (this.isProvider && this.currentToken) { + rpcCall = callTokenRpc(this.currentToken, object, method, params); + } else { + rpcCall = callRpcCall(this.currentNode, object, method, params); + } + + rpcCall.then(function(result) { if (result.success) { self.appendTerminal(JSON.stringify(result.result, null, 2) + '\n\n'); } else { @@ -396,96 +506,54 @@ return view.extend({ }, handleQuickAction: function(object, method) { - if (!this.currentNode) { - ui.addNotification(null, E('p', 'Not connected to any node'), 'error'); + if (!this.currentNode && !this.currentToken) { + ui.addNotification(null, E('p', 'Not connected'), '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'); - }); + this.executeRpc(object, method, '{}'); }, handleReboot: function() { - if (!this.currentNode) { - ui.addNotification(null, E('p', 'Not connected to any node'), 'error'); + if (!this.currentNode && !this.currentToken) { + ui.addNotification(null, E('p', 'Not connected'), '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'); - }); + this.executeRpc('system', 'reboot', '{}'); + this.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'); if (!output) return; output.textContent += text; - output.scrollTop = output.scrollHeight; + var container = document.getElementById('terminal-container'); + if (container) container.scrollTop = container.scrollHeight; }, - updateSessionsList: function() { - var container = document.getElementById('active-sessions-list'); - if (!container) return; - 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') - ]) - ]); - + pollTokens: function() { 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') - ]) - ])); + callTokenList().then(function(result) { + var tokens = result.tokens || []; + var container = document.getElementById('active-tokens-list'); + var section = document.getElementById('active-tokens-section'); + + if (!container || !section) return; + + if (tokens.length > 0) { + section.style.display = 'block'; + container.innerHTML = ''; + container.appendChild(self.renderTokensTable(tokens)); + } else { + section.style.display = 'none'; + } }); - - container.appendChild(table); - }, - - pollSessions: function() { - this.updateSessionsList(); } }); diff --git a/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote b/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote index a0a984c1..3174ca3f 100644 --- a/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote +++ b/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote @@ -292,6 +292,113 @@ method_connect() { json_dump } +#------------------------------------------------------------------------------ +# Token-Based Shared Access +#------------------------------------------------------------------------------ + +# Generate support access token +method_token_generate() { + local ttl permissions + read -r input + json_load "$input" 2>/dev/null + json_get_var ttl ttl + json_get_var permissions permissions + + [ -z "$ttl" ] && ttl=3600 + [ -z "$permissions" ] && permissions="rpc,terminal" + + # Generate token via rttyctl + local output=$($RTTYCTL token generate "$ttl" "$permissions" 2>&1) + + # Extract code from output + local code=$(echo "$output" | grep "Code:" | awk '{print $2}') + local expires=$(echo "$output" | grep "Expires:" | sed 's/.*Expires: //') + + if [ -n "$code" ]; then + json_init + json_add_boolean "success" 1 + json_add_string "code" "$code" + json_add_int "ttl" "$ttl" + json_add_string "permissions" "$permissions" + json_add_string "expires" "$expires" + json_dump + else + printf '{"success":false,"error":"Failed to generate token"}' + fi +} + +# List active tokens +method_token_list() { + $RTTYCTL json-tokens 2>/dev/null || echo '{"tokens":[]}' +} + +# Validate token (for support access) +method_token_validate() { + local code + read -r input + json_load "$input" + json_get_var code code + + [ -z "$code" ] && { + echo '{"valid":false,"error":"Missing code"}' + return + } + + $RTTYCTL token validate "$code" 2>/dev/null +} + +# Revoke token +method_token_revoke() { + local code + read -r input + json_load "$input" + json_get_var code code + + [ -z "$code" ] && { + echo '{"success":false,"error":"Missing code"}' + return + } + + $RTTYCTL token revoke "$code" >/dev/null 2>&1 + + json_init + json_add_boolean "success" 1 + json_dump +} + +# Execute RPC with token authentication (no LuCI session needed) +method_token_rpc() { + local code object method params + read -r input + json_load "$input" + json_get_var code code + json_get_var object object + json_get_var method method + json_get_var params params + + [ -z "$code" ] || [ -z "$object" ] || [ -z "$method" ] && { + echo '{"success":false,"error":"Missing required parameters (code, object, method)"}' + return + } + + [ -z "$params" ] && params="{}" + + # Execute via rttyctl with token auth + local result=$($RTTYCTL token-rpc "$code" "$object" "$method" "$params" 2>&1) + local rc=$? + + if [ $rc -eq 0 ] && [ -n "$result" ]; then + if echo "$result" | jsonfilter -e '@' >/dev/null 2>&1; then + printf '{"success":true,"result":%s}' "$result" + else + printf '{"success":true,"result":"%s"}' "$(echo "$result" | sed 's/"/\\"/g')" + fi + else + local err_msg=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ') + printf '{"success":false,"error":"%s"}' "$err_msg" + fi +} + #------------------------------------------------------------------------------ # Main dispatcher #------------------------------------------------------------------------------ @@ -311,7 +418,12 @@ case "$1" in "get_settings": {}, "set_settings": {"config": "object"}, "replay_session": {"session_id": "integer", "target_node": "string"}, - "connect": {"node_id": "string"} + "connect": {"node_id": "string"}, + "token_generate": {"ttl": 3600, "permissions": "rpc,terminal"}, + "token_list": {}, + "token_validate": {"code": "string"}, + "token_revoke": {"code": "string"}, + "token_rpc": {"code": "string", "object": "string", "method": "string", "params": "string"} } EOF ;; @@ -329,6 +441,11 @@ EOF set_settings) method_set_settings ;; replay_session) method_replay_session ;; connect) method_connect ;; + token_generate) method_token_generate ;; + token_list) method_token_list ;; + token_validate) method_token_validate ;; + token_revoke) method_token_revoke ;; + token_rpc) method_token_rpc ;; *) echo '{"error":"Unknown method"}' ;; diff --git a/package/secubox/luci-app-rtty-remote/root/usr/share/rpcd/acl.d/luci-rtty-remote.json b/package/secubox/luci-app-rtty-remote/root/usr/share/rpcd/acl.d/luci-rtty-remote.json index 0cb59400..e0eae02c 100644 --- a/package/secubox/luci-app-rtty-remote/root/usr/share/rpcd/acl.d/luci-rtty-remote.json +++ b/package/secubox/luci-app-rtty-remote/root/usr/share/rpcd/acl.d/luci-rtty-remote.json @@ -9,7 +9,9 @@ "get_node", "rpc_list", "get_sessions", - "get_settings" + "get_settings", + "token_list", + "token_validate" ] }, "uci": ["rtty-remote"] @@ -22,7 +24,10 @@ "server_stop", "set_settings", "replay_session", - "connect" + "connect", + "token_generate", + "token_revoke", + "token_rpc" ] }, "uci": ["rtty-remote"] diff --git a/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl b/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl index cbc19087..743be9a9 100644 --- a/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl +++ b/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl @@ -16,6 +16,7 @@ VERSION="0.1.0" CONFIG_FILE="/etc/config/rtty-remote" SESSION_DB="/srv/rtty-remote/sessions.db" CACHE_DIR="/tmp/rtty-remote" +TOKEN_DIR="/tmp/rtty-remote/tokens" LOG_FILE="/var/log/rtty-remote.log" # Load libraries @@ -44,9 +45,234 @@ die() { ensure_dirs() { mkdir -p "$CACHE_DIR" 2>/dev/null + mkdir -p "$TOKEN_DIR" 2>/dev/null mkdir -p "$(dirname "$SESSION_DB")" 2>/dev/null } +#------------------------------------------------------------------------------ +# Token-Based Shared Access +#------------------------------------------------------------------------------ + +generate_token_code() { + # Generate 6-character alphanumeric code + # Use hexdump if available, fallback to awk-based pseudo-random + local chars="ABCDEFGHJKLMNPQRSTUVWXYZ23456789" + local code="" + + if command -v hexdump >/dev/null 2>&1; then + # Use hexdump for true randomness + local hex=$(head -c 6 /dev/urandom | hexdump -e '6/1 "%02x"') + local i=0 + while [ $i -lt 6 ]; do + local byte=$(printf "%d" "0x$(echo "$hex" | cut -c$((i*2+1))-$((i*2+2)))") + local idx=$((byte % 32)) + code="${code}$(echo "$chars" | cut -c$((idx + 1)))" + i=$((i + 1)) + done + else + # Fallback: use awk with pseudo-random seed from /proc/sys/kernel/random/uuid + local seed=$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-' | cut -c1-8) + seed=$(printf "%d" "0x$seed" 2>/dev/null || echo $(($(date +%s) % 65536))) + code=$(awk -v seed="$seed" -v chars="$chars" 'BEGIN { + srand(seed) + for (i=1; i<=6; i++) { + idx = int(rand() * 32) + 1 + printf "%s", substr(chars, idx, 1) + } + print "" + }') + fi + + echo "$code" +} + +cmd_token_generate() { + local ttl="${1:-3600}" # Default 1 hour + local permissions="${2:-rpc,terminal}" + + local code=$(generate_token_code) + local now=$(date +%s) + local expires=$((now + ttl)) + local node_name=$(uci -q get system.@system[0].hostname || echo "secubox") + local node_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") + + # Store token + cat > "$TOKEN_DIR/$code" << EOF +{ + "code": "$code", + "created": $now, + "expires": $expires, + "ttl": $ttl, + "node_id": "$node_name", + "node_ip": "$node_ip", + "permissions": "$permissions", + "used": 0 +} +EOF + + log "info" "Generated token $code (expires in ${ttl}s)" + + echo "Support Access Token Generated" + echo "==============================" + echo "" + echo " Code: $code" + echo " Expires: $(date -d "@$expires" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r $expires "+%Y-%m-%d %H:%M:%S")" + echo " TTL: ${ttl}s" + echo " Access: $permissions" + echo "" + echo "Share this code with the support person." +} + +cmd_token_validate() { + local code="$1" + [ -z "$code" ] && { echo '{"valid":false,"error":"Missing code"}'; return 1; } + + # Normalize code (uppercase, remove spaces) + code=$(echo "$code" | tr 'a-z' 'A-Z' | tr -d ' -') + + local token_file="$TOKEN_DIR/$code" + + if [ ! -f "$token_file" ]; then + echo '{"valid":false,"error":"Invalid code"}' + return 1 + fi + + local now=$(date +%s) + local expires=$(jsonfilter -i "$token_file" -e '@.expires') + + if [ "$now" -gt "$expires" ]; then + rm -f "$token_file" + echo '{"valid":false,"error":"Token expired"}' + return 1 + fi + + # Mark as used + local used=$(jsonfilter -i "$token_file" -e '@.used') + used=$((used + 1)) + local tmp_file="${token_file}.tmp" + jsonfilter -i "$token_file" -e '@' | sed "s/\"used\": [0-9]*/\"used\": $used/" > "$tmp_file" + mv "$tmp_file" "$token_file" + + # Return token info + cat "$token_file" +} + +cmd_token_list() { + echo "Active Support Tokens" + echo "=====================" + echo "" + + local now=$(date +%s) + local count=0 + + for token_file in "$TOKEN_DIR"/*; do + [ -f "$token_file" ] || continue + + local code=$(basename "$token_file") + local expires=$(jsonfilter -i "$token_file" -e '@.expires' 2>/dev/null) + local created=$(jsonfilter -i "$token_file" -e '@.created' 2>/dev/null) + local used=$(jsonfilter -i "$token_file" -e '@.used' 2>/dev/null) + local perms=$(jsonfilter -i "$token_file" -e '@.permissions' 2>/dev/null) + + [ -z "$expires" ] && continue + + if [ "$now" -gt "$expires" ]; then + rm -f "$token_file" + continue + fi + + local remaining=$((expires - now)) + local mins=$((remaining / 60)) + + printf " %-8s %3dm left used:%d [%s]\n" "$code" "$mins" "$used" "$perms" + count=$((count + 1)) + done + + [ $count -eq 0 ] && echo " No active tokens" + echo "" +} + +cmd_token_revoke() { + local code="$1" + [ -z "$code" ] && die "Usage: rttyctl token revoke " + + code=$(echo "$code" | tr 'a-z' 'A-Z' | tr -d ' -') + + if [ -f "$TOKEN_DIR/$code" ]; then + rm -f "$TOKEN_DIR/$code" + echo "Token $code revoked" + log "info" "Token $code revoked" + else + echo "Token $code not found" + fi +} + +cmd_token_json_list() { + local now=$(date +%s) + + printf '{"tokens":[' + local first=1 + + for token_file in "$TOKEN_DIR"/*; do + [ -f "$token_file" ] || continue + + local expires=$(jsonfilter -i "$token_file" -e '@.expires' 2>/dev/null) + [ -z "$expires" ] && continue + + if [ "$now" -gt "$expires" ]; then + rm -f "$token_file" + continue + fi + + [ $first -eq 0 ] && printf ',' + cat "$token_file" + first=0 + done + + printf ']}' +} + +# Token-authenticated RPC call (for support access) +cmd_token_rpc() { + local code="$1" + local object="$2" + local method="$3" + shift 3 + local params="$*" + + [ -z "$code" ] || [ -z "$object" ] || [ -z "$method" ] && { + echo "Usage: rttyctl token-rpc [params]" + return 1 + } + + # Validate token + local validation=$(cmd_token_validate "$code") + local valid=$(echo "$validation" | jsonfilter -e '@.valid' 2>/dev/null) + + if [ "$valid" = "false" ]; then + echo "Error: $(echo "$validation" | jsonfilter -e '@.error')" + return 1 + fi + + # Check permissions + local perms=$(echo "$validation" | jsonfilter -e '@.permissions' 2>/dev/null) + case "$perms" in + *rpc*) ;; + *) echo "Error: Token does not have RPC permission"; return 1 ;; + esac + + # Execute RPC locally (token grants local access) + local result=$(ubus call "$object" "$method" "${params:-{}}" 2>&1) + local rc=$? + + if [ $rc -eq 0 ]; then + echo "$result" + else + echo "Error: $result" + return 1 + fi +} + get_config() { local section="$1" local option="$2" @@ -576,6 +802,13 @@ Authentication: auth Authenticate to remote node revoke Revoke authentication +Shared Access Tokens: + token generate [ttl] [perms] Generate support token (default: 3600s, rpc,terminal) + token list List active tokens + token validate Validate a token + token revoke Revoke a token + token-rpc [params] Execute RPC with token auth + JSON Output (for RPCD): json-status Status as JSON json-nodes Nodes as JSON @@ -646,6 +879,32 @@ case "$1" in json-nodes) cmd_json_nodes ;; + json-tokens) + cmd_token_json_list + ;; + token) + case "$2" in + generate) + cmd_token_generate "$3" "$4" + ;; + list) + cmd_token_list + ;; + validate) + cmd_token_validate "$3" + ;; + revoke) + cmd_token_revoke "$3" + ;; + *) + echo "Usage: rttyctl token generate|list|validate|revoke" + ;; + esac + ;; + token-rpc) + shift + cmd_token_rpc "$@" + ;; -h|--help|help) show_help ;;