From fa02d44f8df7c8972a5cb154705a846854db1fff Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 9 Jan 2026 13:13:21 +0100 Subject: [PATCH] feat: Replace RustDesk with rtty remote access in system-hub - Add rtty support for reverse proxy terminal access to relay server - Add ttyd web console with embedded iframe terminal - Fix pgrep -x issues by replacing with pidof (BusyBox compatible) - Update API.js to v0.4.0 with rtty parameters - Rewrite remote.js view with rtty configuration UI: - Server host/port/token/description fields - SSL/TLS toggle - Connect/Disconnect controls - Device ID display (auto-generated from MAC) - Add RPCD methods: ttyd_status, ttyd_install, ttyd_start, ttyd_stop, ttyd_configure - Update ACL permissions for new methods Co-Authored-By: Claude Opus 4.5 --- .../luci-static/resources/system-hub/api.js | 49 +- .../resources/view/system-hub/remote.js | 408 +++++++++++++---- .../root/usr/libexec/rpcd/luci.system-hub | 418 ++++++++++++++---- .../share/rpcd/acl.d/luci-app-system-hub.json | 9 +- 4 files changed, 719 insertions(+), 165 deletions(-) diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js index 48a9d88c..387b7cc5 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js @@ -6,11 +6,11 @@ * System Hub API * Package: luci-app-system-hub * RPCD object: luci.system-hub - * Version: 0.3.6 + * Version: 0.4.0 */ // Debug log to verify correct version is loaded -console.log('🔧 System Hub API v0.3.6 loaded at', new Date().toISOString()); +console.log('🔧 System Hub API v0.4.0 loaded at', new Date().toISOString()); var callStatus = rpc.declare({ object: 'luci.system-hub', @@ -183,7 +183,7 @@ var callRemoteInstall = rpc.declare({ var callRemoteConfigure = rpc.declare({ object: 'luci.system-hub', method: 'remote_configure', - params: ['relay_server', 'relay_key', 'rustdesk_enabled'], + params: ['host', 'port', 'id', 'description', 'ssl', 'token'], expect: {} }); @@ -203,7 +203,39 @@ var callRemoteServiceAction = rpc.declare({ var callRemoteSaveSettings = rpc.declare({ object: 'luci.system-hub', method: 'remote_save_settings', - params: ['allow_unattended', 'require_approval', 'notify_on_connect'], + params: ['host', 'port', 'id', 'description', 'ssl', 'token'], + expect: {} +}); + +// TTYD Web Console +var callTtydStatus = rpc.declare({ + object: 'luci.system-hub', + method: 'ttyd_status', + expect: {} +}); + +var callTtydInstall = rpc.declare({ + object: 'luci.system-hub', + method: 'ttyd_install', + expect: {} +}); + +var callTtydStart = rpc.declare({ + object: 'luci.system-hub', + method: 'ttyd_start', + expect: {} +}); + +var callTtydStop = rpc.declare({ + object: 'luci.system-hub', + method: 'ttyd_stop', + expect: {} +}); + +var callTtydConfigure = rpc.declare({ + object: 'luci.system-hub', + method: 'ttyd_configure', + params: ['port', 'interface'], expect: {} }); @@ -287,5 +319,14 @@ return baseclass.extend({ }, remoteSaveSettings: function(data) { return callRemoteSaveSettings(data); + }, + + // TTYD Web Console + ttydStatus: callTtydStatus, + ttydInstall: callTtydInstall, + ttydStart: callTtydStart, + ttydStop: callTtydStop, + ttydConfigure: function(data) { + return callTtydConfigure(data); } }); diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js index de29034d..c5833c08 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js @@ -14,106 +14,229 @@ Theme.init({ language: shLang }); return view.extend({ load: function() { - return API.remoteStatus(); + return Promise.all([ + API.remoteStatus(), + API.ttydStatus() + ]); }, - render: function(remote) { - this.remote = remote || {}; + render: function(data) { + var remote = data[0] || {}; + var ttyd = data[1] || {}; + this.remote = remote; + this.ttyd = ttyd; var view = E('div', { 'class': 'system-hub-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), ThemeAssets.stylesheet('common.css'), ThemeAssets.stylesheet('dashboard.css'), HubNav.renderTabs('remote'), - - // RustDesk Section + + // rtty Remote Access Section E('div', { 'class': 'sh-card sh-remote-card' }, [ E('div', { 'class': 'sh-card-header' }, [ - E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '🖥️'), 'RustDesk - Assistance à Distance' ]), - E('div', { 'class': 'sh-card-badge' }, remote.enabled ? 'Actif' : 'Inactif') + E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '🔗'), _('rtty - Remote Terminal') ]), + E('div', { 'class': 'sh-card-badge', 'id': 'rtty-status-badge' }, remote.running ? _('Connected') : _('Disconnected')) ]), E('div', { 'class': 'sh-card-body' }, [ - // RustDesk ID + // Device ID E('div', { 'class': 'sh-remote-id' }, [ - E('div', { 'class': 'sh-remote-id-icon' }, '🖥️'), + E('div', { 'class': 'sh-remote-id-icon' }, '🆔'), E('div', {}, [ - E('div', { 'class': 'sh-remote-id-value', 'id': 'remote-id-value' }, remote.id || '--- --- ---'), - E('div', { 'class': 'sh-remote-id-label' }, 'ID RustDesk - Communiquez ce code au support') + E('div', { 'class': 'sh-remote-id-value', 'id': 'rtty-device-id' }, remote.id || _('Not configured')), + E('div', { 'class': 'sh-remote-id-label' }, _('Device ID - Share this with support')) ]) ]), - - // Settings - this.renderToggle('🔒', 'Accès sans surveillance', 'Permettre la connexion sans approbation', remote.allow_unattended, 'allow_unattended'), - this.renderToggle('✅', 'Approbation requise', 'Confirmer chaque connexion entrante', remote.require_approval, 'require_approval'), - this.renderToggle('🔔', 'Notification de connexion', 'Recevoir une alerte à chaque session', remote.notify_on_connect, 'notify_on_connect'), - + + // Configuration fields + E('div', { 'class': 'sh-form-grid', 'style': 'margin-top: 16px;' }, [ + // Server Host + E('div', { 'class': 'sh-form-group' }, [ + E('label', { 'class': 'sh-form-label' }, _('Server Host')), + E('input', { + 'type': 'text', + 'class': 'sh-form-input', + 'id': 'rtty-host', + 'placeholder': 'rttys.example.com', + 'value': remote.host || '' + }) + ]), + // Server Port + E('div', { 'class': 'sh-form-group' }, [ + E('label', { 'class': 'sh-form-label' }, _('Server Port')), + E('input', { + 'type': 'number', + 'class': 'sh-form-input', + 'id': 'rtty-port', + 'placeholder': '5912', + 'value': remote.port || 5912 + }) + ]), + // Token + E('div', { 'class': 'sh-form-group' }, [ + E('label', { 'class': 'sh-form-label' }, _('Access Token')), + E('input', { + 'type': 'password', + 'class': 'sh-form-input', + 'id': 'rtty-token', + 'placeholder': _('Optional authentication token'), + 'value': remote.token || '' + }) + ]), + // Device Description + E('div', { 'class': 'sh-form-group' }, [ + E('label', { 'class': 'sh-form-label' }, _('Description')), + E('input', { + 'type': 'text', + 'class': 'sh-form-input', + 'id': 'rtty-description', + 'placeholder': _('Device description'), + 'value': remote.description || '' + }) + ]) + ]), + + // SSL Toggle + this.renderToggle('🔒', _('Use SSL/TLS'), _('Encrypt connection to relay server'), remote.ssl, 'rtty-ssl'), + // Status !remote.installed ? E('div', { 'style': 'padding: 16px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-top: 16px;' }, [ E('span', { 'style': 'font-size: 20px; margin-right: 12px;' }, '⚠️'), - E('span', {}, 'RustDesk n\'est pas installé. '), - E('a', { 'href': '#', 'style': 'color: #6366f1;', 'click': L.bind(this.installRustdesk, this) }, 'Installer maintenant') + E('span', {}, _('rtty is not installed.')), + E('a', { 'href': '#', 'style': 'color: #6366f1; margin-left: 8px;', 'click': L.bind(this.installRtty, this) }, _('Install now')) ]) : E('div', { 'style': 'padding: 10px; background: rgba(34,197,94,0.12); border-radius: 10px; margin-top: 16px;' }, [ E('span', { 'style': 'font-size: 20px; margin-right: 12px;' }, remote.running ? '🟢' : '🟠'), - E('span', {}, remote.running ? 'Service RustDesk en cours d\'exécution' : 'Service installé mais arrêté') + E('span', {}, remote.running ? _('Connected to relay server') : _('Installed but not connected')) ]), - + // Actions - E('div', { 'class': 'sh-btn-group' }, [ - E('button', { + E('div', { 'class': 'sh-btn-group', 'style': 'margin-top: 16px;' }, [ + E('button', { 'class': 'sh-btn sh-btn-primary', - 'click': L.bind(this.showCredentials, this) - }, [ '🔑 Identifiants' ]), - E('button', { + 'id': 'rtty-save-btn', + 'click': L.bind(this.saveRttySettings, this) + }, [ '💾 ', _('Save Settings') ]), + E('button', { 'class': 'sh-btn', - 'click': L.bind(this.toggleService, this) - }, [ remote.running ? '⏹️ Arrêter' : '▶️ Démarrer' ]) + 'id': 'rtty-toggle-btn', + 'click': L.bind(this.toggleRttyService, this) + }, [ remote.running ? '⏹️ ' + _('Disconnect') : '▶️ ' + _('Connect') ]), + E('button', { + 'class': 'sh-btn', + 'click': L.bind(this.showCredentials, this) + }, [ '🔑 ', _('Show Credentials') ]) ]) ]) ]), - + // SSH Section E('div', { 'class': 'sh-card' }, [ E('div', { 'class': 'sh-card-header' }, [ - E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '🔐'), 'Accès SSH' ]) + E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '🔐'), _('SSH Access') ]) ]), E('div', { 'class': 'sh-card-body' }, [ E('div', { 'class': 'sh-sysinfo-grid' }, [ E('div', { 'class': 'sh-sysinfo-item' }, [ - E('span', { 'class': 'sh-sysinfo-label' }, 'Status'), - E('span', { 'class': 'sh-sysinfo-value', 'style': 'color: #22c55e;' }, 'Actif') + E('span', { 'class': 'sh-sysinfo-label' }, _('Status')), + E('span', { 'class': 'sh-sysinfo-value', 'style': 'color: #22c55e;' }, _('Active')) ]), E('div', { 'class': 'sh-sysinfo-item' }, [ - E('span', { 'class': 'sh-sysinfo-label' }, 'Port'), + E('span', { 'class': 'sh-sysinfo-label' }, _('Port')), E('span', { 'class': 'sh-sysinfo-value' }, '22') ]) ]), E('div', { 'style': 'margin-top: 16px; padding: 14px; background: #0a0a0f; border-radius: 8px; font-family: monospace; font-size: 12px; color: #a0a0b0;' }, [ - 'ssh root@', E('span', { 'style': 'color: #6366f1;' }, '192.168.1.1') + 'ssh root@', E('span', { 'style': 'color: #6366f1;' }, window.location.hostname) ]) ]) ]), - + + // Web Console (ttyd) Section + E('div', { 'class': 'sh-card sh-webconsole-card' }, [ + E('div', { 'class': 'sh-card-header' }, [ + E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '💻'), _('Web Console') ]), + E('div', { 'class': 'sh-card-badge', 'id': 'ttyd-status-badge' }, ttyd.running ? _('Running') : _('Stopped')) + ]), + E('div', { 'class': 'sh-card-body' }, [ + // Status info + E('div', { 'class': 'sh-sysinfo-grid' }, [ + E('div', { 'class': 'sh-sysinfo-item' }, [ + E('span', { 'class': 'sh-sysinfo-label' }, _('Status')), + E('span', { 'class': 'sh-sysinfo-value', 'id': 'ttyd-status-text', 'style': ttyd.running ? 'color: #22c55e;' : 'color: #f59e0b;' }, + ttyd.installed ? (ttyd.running ? _('Running') : _('Stopped')) : _('Not Installed')) + ]), + E('div', { 'class': 'sh-sysinfo-item' }, [ + E('span', { 'class': 'sh-sysinfo-label' }, _('Port')), + E('span', { 'class': 'sh-sysinfo-value' }, ttyd.port || 7681) + ]) + ]), + + // Install warning or console iframe + !ttyd.installed ? E('div', { 'style': 'padding: 16px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 3px solid #f59e0b; margin-top: 16px;' }, [ + E('span', { 'style': 'font-size: 20px; margin-right: 12px;' }, '⚠️'), + E('span', {}, _('Web Console (ttyd) is not installed.')), + E('a', { 'href': '#', 'style': 'color: #6366f1; margin-left: 8px;', 'click': L.bind(this.installTtyd, this) }, _('Install now')) + ]) : (ttyd.running ? + // Console iframe when running + E('div', { 'id': 'ttyd-console-container', 'style': 'margin-top: 16px; border-radius: 8px; overflow: hidden; background: #0a0a0f; border: 1px solid rgba(255,255,255,0.1);' }, [ + E('iframe', { + 'id': 'ttyd-iframe', + 'src': 'http://' + window.location.hostname + ':' + (ttyd.port || 7681), + 'style': 'width: 100%; height: 400px; border: none; background: #0a0a0f;', + 'title': 'Web Console' + }) + ]) : + // Start prompt when stopped + E('div', { 'style': 'padding: 20px; background: rgba(99, 102, 241, 0.1); border-radius: 10px; margin-top: 16px; text-align: center;' }, [ + E('span', { 'style': 'font-size: 40px; display: block; margin-bottom: 12px;' }, '💻'), + E('p', { 'style': 'margin: 0 0 16px 0; color: #a0a0b0;' }, _('Web Console is ready. Click Start to open the terminal.')), + E('button', { + 'class': 'sh-btn sh-btn-primary', + 'click': L.bind(this.startTtyd, this) + }, [ '▶️ ', _('Start Console') ]) + ]) + ), + + // Actions + ttyd.installed ? E('div', { 'class': 'sh-btn-group', 'style': 'margin-top: 16px;' }, [ + E('button', { + 'class': 'sh-btn sh-btn-primary', + 'id': 'ttyd-toggle-btn', + 'click': L.bind(this.toggleTtyd, this) + }, [ ttyd.running ? '⏹️ ' + _('Stop') : '▶️ ' + _('Start') ]), + ttyd.running ? E('button', { + 'class': 'sh-btn', + 'click': L.bind(this.openTtydFullscreen, this) + }, [ '🔲 ', _('Fullscreen') ]) : '', + E('button', { + 'class': 'sh-btn', + 'click': L.bind(this.refreshTtydStatus, this) + }, [ '🔄 ', _('Refresh') ]) + ]) : '' + ]) + ]), + // Support Contact (static) E('div', { 'class': 'sh-card' }, [ E('div', { 'class': 'sh-card-header' }, [ - E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '📞'), 'Contact Support' ]) + E('div', { 'class': 'sh-card-title' }, [ E('span', { 'class': 'sh-card-title-icon' }, '📞'), _('Contact Support') ]) ]), E('div', { 'class': 'sh-card-body' }, [ E('div', { 'class': 'sh-sysinfo-grid' }, [ E('div', { 'class': 'sh-sysinfo-item' }, [ - E('span', { 'class': 'sh-sysinfo-label' }, 'Fournisseur'), + E('span', { 'class': 'sh-sysinfo-label' }, _('Provider')), E('span', { 'class': 'sh-sysinfo-value' }, 'CyberMind.fr') ]), E('div', { 'class': 'sh-sysinfo-item' }, [ - E('span', { 'class': 'sh-sysinfo-label' }, 'Email'), + E('span', { 'class': 'sh-sysinfo-label' }, _('Email')), E('span', { 'class': 'sh-sysinfo-value' }, 'support@cybermind.fr') ]), E('div', { 'class': 'sh-sysinfo-item' }, [ - E('span', { 'class': 'sh-sysinfo-label' }, 'Téléphone'), + E('span', { 'class': 'sh-sysinfo-label' }, _('Phone')), E('span', { 'class': 'sh-sysinfo-value' }, '+33 1 23 45 67 89') ]), E('div', { 'class': 'sh-sysinfo-item' }, [ - E('span', { 'class': 'sh-sysinfo-label' }, 'Website'), + E('span', { 'class': 'sh-sysinfo-label' }, _('Website')), E('span', { 'class': 'sh-sysinfo-value' }, 'https://cybermind.fr') ]) ]) @@ -124,8 +247,8 @@ return view.extend({ return view; }, - renderToggle: function(icon, label, desc, enabled, field) { - return E('div', { 'class': 'sh-toggle' }, [ + renderToggle: function(icon, label, desc, enabled, id) { + return E('div', { 'class': 'sh-toggle', 'style': 'margin-top: 16px;' }, [ E('div', { 'class': 'sh-toggle-info' }, [ E('span', { 'class': 'sh-toggle-icon' }, icon), E('div', {}, [ @@ -133,63 +256,58 @@ return view.extend({ E('div', { 'class': 'sh-toggle-desc' }, desc) ]) ]), - E('div', { + E('div', { 'class': 'sh-toggle-switch' + (enabled ? ' active' : ''), - 'data-field': field, - 'click': L.bind(function(ev) { + 'id': id, + 'click': function(ev) { ev.target.classList.toggle('active'); - this.saveSettings(); - }, this) + } }) ]); }, showCredentials: function() { - ui.showModal(_('Identifiants RustDesk'), [ - E('p', {}, 'Récupération en cours…'), + ui.showModal(_('rtty Credentials'), [ + E('p', {}, _('Retrieving credentials...')), E('div', { 'class': 'spinning' }) ]); - API.remoteCredentials().then(function(result) { + API.remoteCredentials().then(L.bind(function(result) { ui.hideModal(); - ui.showModal(_('Identifiants RustDesk'), [ - E('div', { 'style': 'font-size:18px; margin-bottom:8px;' }, 'ID: ' + (result.id || '---')), - E('div', { 'style': 'font-size:18px;' }, 'Mot de passe: ' + (result.password || '---')), + ui.showModal(_('rtty Credentials'), [ + E('div', { 'style': 'font-size:16px; margin-bottom:12px;' }, [ + E('strong', {}, _('Device ID:')), ' ', (result.id || _('Not configured')) + ]), + E('div', { 'style': 'font-size:16px; margin-bottom:12px;' }, [ + E('strong', {}, _('Token:')), ' ', (result.token || _('No token set')) + ]), + E('p', { 'style': 'color: #a0a0b0; font-size: 13px; margin-top: 16px;' }, + _('Share the Device ID with support to allow remote access via the rtty relay server.')), E('div', { 'class': 'sh-btn-group', 'style': 'margin-top:16px;' }, [ - E('button', { 'class': 'sh-btn sh-btn-primary', 'click': ui.hideModal }, 'Fermer') + E('button', { 'class': 'sh-btn sh-btn-primary', 'click': ui.hideModal }, _('Close')) ]) ]); - }).catch(function(err) { + }, this)).catch(function(err) { ui.hideModal(); ui.addNotification(null, E('p', {}, err.message || err), 'error'); }); }, - toggleService: function() { + toggleRttyService: function() { if (!this.remote || !this.remote.installed) return; var action = this.remote.running ? 'stop' : 'start'; - API.remoteServiceAction(action).then(L.bind(function(res) { - if (res.success) { - this.reload(); - ui.addNotification(null, E('p', {}, '✅ ' + action), 'info'); - } else { - ui.addNotification(null, E('p', {}, res.error || 'Action impossible'), 'error'); - } - }, this)); - }, - installRustdesk: function(ev) { - ev.preventDefault(); - ui.showModal(_('Installation'), [ - E('p', {}, 'Installation de RustDesk…'), + ui.showModal(action === 'start' ? _('Connecting...') : _('Disconnecting...'), [ + E('p', {}, action === 'start' ? _('Connecting to relay server...') : _('Stopping rtty service...')), E('div', { 'class': 'spinning' }) ]); - API.remoteInstall().then(L.bind(function(result) { + + API.remoteServiceAction(action).then(L.bind(function(res) { ui.hideModal(); - if (result.success) { - ui.addNotification(null, E('p', {}, result.message || 'Installé'), 'info'); + if (res.success) { + ui.addNotification(null, E('p', {}, '✅ ' + (res.message || action)), 'info'); this.reload(); } else { - ui.addNotification(null, E('p', {}, result.error || 'Installation impossible'), 'error'); + ui.addNotification(null, E('p', {}, res.error || _('Action failed')), 'error'); } }, this)).catch(function(err) { ui.hideModal(); @@ -197,15 +315,60 @@ return view.extend({ }); }, - saveSettings: function() { - var allow = document.querySelector('[data-field="allow_unattended"]').classList.contains('active') ? 1 : 0; - var require = document.querySelector('[data-field="require_approval"]').classList.contains('active') ? 1 : 0; - var notify = document.querySelector('[data-field="notify_on_connect"]').classList.contains('active') ? 1 : 0; + saveRttySettings: function() { + var host = document.getElementById('rtty-host').value; + var port = parseInt(document.getElementById('rtty-port').value) || 5912; + var token = document.getElementById('rtty-token').value; + var description = document.getElementById('rtty-description').value; + var ssl = document.getElementById('rtty-ssl').classList.contains('active') ? 1 : 0; + + if (!host) { + ui.addNotification(null, E('p', {}, _('Server host is required')), 'error'); + return; + } + + ui.showModal(_('Saving Settings'), [ + E('p', {}, _('Saving rtty configuration...')), + E('div', { 'class': 'spinning' }) + ]); API.remoteSaveSettings({ - allow_unattended: allow, - require_approval: require, - notify_on_connect: notify + host: host, + port: port, + token: token, + description: description, + ssl: ssl + }).then(L.bind(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', {}, _('Settings saved successfully')), 'info'); + this.reload(); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to save settings')), 'error'); + } + }, this)).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + }, + + installRtty: function(ev) { + ev.preventDefault(); + ui.showModal(_('Installing rtty'), [ + E('p', {}, _('Installing rtty-openssl package...')), + E('div', { 'class': 'spinning' }) + ]); + API.remoteInstall().then(L.bind(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', {}, result.message || _('Installed successfully')), 'info'); + this.reload(); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Installation failed')), 'error'); + } + }, this)).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); }); }, @@ -219,6 +382,91 @@ return view.extend({ }, this)); }, + // TTYD Web Console methods + installTtyd: function(ev) { + ev.preventDefault(); + ui.showModal(_('Installing Web Console'), [ + E('p', {}, _('Installing ttyd...')), + E('div', { 'class': 'spinning' }) + ]); + API.ttydInstall().then(L.bind(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', {}, _('Web Console installed successfully')), 'info'); + this.reload(); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Installation failed')), 'error'); + } + }, this)).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + }, + + startTtyd: function() { + ui.showModal(_('Starting Web Console'), [ + E('p', {}, _('Starting ttyd service...')), + E('div', { 'class': 'spinning' }) + ]); + API.ttydStart().then(L.bind(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', {}, _('Web Console started on port ') + result.port), 'info'); + this.reload(); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to start')), 'error'); + } + }, this)).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, err.message || err), 'error'); + }); + }, + + stopTtyd: function() { + API.ttydStop().then(L.bind(function(result) { + if (result.success) { + ui.addNotification(null, E('p', {}, _('Web Console stopped')), 'info'); + this.reload(); + } else { + ui.addNotification(null, E('p', {}, result.error || _('Failed to stop')), 'error'); + } + }, this)); + }, + + toggleTtyd: function() { + if (this.ttyd && this.ttyd.running) { + this.stopTtyd(); + } else { + this.startTtyd(); + } + }, + + openTtydFullscreen: function() { + var port = (this.ttyd && this.ttyd.port) || 7681; + window.open('http://' + window.location.hostname + ':' + port, '_blank'); + }, + + refreshTtydStatus: function() { + API.ttydStatus().then(L.bind(function(status) { + this.ttyd = status; + var badge = document.getElementById('ttyd-status-badge'); + var text = document.getElementById('ttyd-status-text'); + var btn = document.getElementById('ttyd-toggle-btn'); + + if (badge) badge.textContent = status.running ? _('Running') : _('Stopped'); + if (text) { + text.textContent = status.installed ? (status.running ? _('Running') : _('Stopped')) : _('Not Installed'); + text.style.color = status.running ? '#22c55e' : '#f59e0b'; + } + if (btn) btn.innerHTML = status.running ? '⏹️ ' + _('Stop') : '▶️ ' + _('Start'); + + // Reload to update iframe visibility + if (status.running !== this.ttyd.running) { + this.reload(); + } + }, this)); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub b/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub index b92540dd..2875df1f 100755 --- a/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub +++ b/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub @@ -147,7 +147,7 @@ get_system_info() { # NTP sync status local ntp_ok=0 - if [ -f /var/state/ntpd ] || pgrep -x ntpd >/dev/null 2>&1 || pgrep -x chronyd >/dev/null 2>&1; then + if [ -f /var/state/ntpd ] || pidof ntpd >/dev/null 2>&1 || pidof chronyd >/dev/null 2>&1; then # Check if time seems reasonable (after year 2020) local year=$(date +%Y) [ "$year" -ge 2020 ] && ntp_ok=1 @@ -1225,41 +1225,54 @@ get_diagnostic_profile() { } remote_status() { - local section="system-hub.remote" - local enabled=$(uci -q get $section.rustdesk_enabled || echo 0) - local relay_server=$(uci -q get $section.rustdesk_server || echo "") - local relay_key=$(uci -q get $section.rustdesk_key || echo "") - local stored_id=$(uci -q get $section.rustdesk_id || echo "") - local stored_password=$(uci -q get $section.rustdesk_password || echo "") - local allow_unattended=$(uci -q get $section.allow_unattended || echo 0) - local require_approval=$(uci -q get $section.require_approval || echo 1) - local notify_on_connect=$(uci -q get $section.notify_on_connect || echo 1) - local installed=0 - command -v rustdesk >/dev/null 2>&1 && installed=1 - local running=0 - if [ -x /etc/init.d/rustdesk ]; then - /etc/init.d/rustdesk status >/dev/null 2>&1 && running=1 + local enabled=0 + + # Check if rtty is installed + command -v rtty >/dev/null 2>&1 && installed=1 + + # Check if running + if pidof rtty >/dev/null 2>&1; then + running=1 + fi + + # Check if enabled + if [ -x /etc/init.d/rtty ]; then + [ -f /etc/rc.d/S*rtty ] && enabled=1 + fi + + # Get config from UCI + local host=$(uci -q get rtty.@rtty[0].host || echo "") + local port=$(uci -q get rtty.@rtty[0].port || echo "5912") + local device_id=$(uci -q get rtty.@rtty[0].id || echo "") + local description=$(uci -q get rtty.@rtty[0].description || echo "") + local ssl=$(uci -q get rtty.@rtty[0].ssl || echo "0") + local token=$(uci -q get rtty.@rtty[0].token || echo "") + + # Auto-generate ID from MAC if not set + if [ -z "$device_id" ]; then + local iface=$(uci -q get rtty.@rtty[0].interface || echo "lan") + device_id=$(cat /sys/class/net/br-$iface/address 2>/dev/null | tr -d ':' || cat /sys/class/net/eth0/address 2>/dev/null | tr -d ':' || echo "") fi json_init json_add_boolean "installed" "$installed" json_add_boolean "running" "$running" json_add_boolean "enabled" "$enabled" - json_add_string "server" "$relay_server" - json_add_string "key" "$relay_key" - json_add_string "id" "$stored_id" - json_add_string "password" "$stored_password" - json_add_boolean "allow_unattended" "$allow_unattended" - json_add_boolean "require_approval" "$require_approval" - json_add_boolean "notify_on_connect" "$notify_on_connect" + json_add_string "host" "$host" + json_add_int "port" "$port" + json_add_string "id" "$device_id" + json_add_string "description" "$description" + json_add_boolean "ssl" "$ssl" + json_add_string "token" "$token" json_dump } remote_install() { json_init - if command -v rustdesk >/dev/null 2>&1; then + + if command -v rtty >/dev/null 2>&1; then json_add_boolean "success" 0 json_add_string "error" "already_installed" json_dump @@ -1273,12 +1286,12 @@ remote_install() { return fi - opkg update >/tmp/rustdesk-install.log 2>&1 - if opkg install rustdesk >>/tmp/rustdesk-install.log 2>&1; then + opkg update >/tmp/rtty-install.log 2>&1 + if opkg install rtty-openssl >>/tmp/rtty-install.log 2>&1; then json_add_boolean "success" 1 - json_add_string "message" "RustDesk installed" + json_add_string "message" "rtty installed successfully" else - local err="$(tail -n 20 /tmp/rustdesk-install.log 2>/dev/null)" + local err="$(tail -n 20 /tmp/rtty-install.log 2>/dev/null)" json_add_boolean "success" 0 json_add_string "error" "${err:-install_failed}" fi @@ -1288,36 +1301,28 @@ remote_install() { remote_configure() { read input json_load "$input" - local section="system-hub.remote" - local server key enabled - json_get_var server relay_server - json_get_var key relay_key - json_get_var enabled rustdesk_enabled - [ -n "$server" ] && uci set $section.rustdesk_server="$server" - [ -n "$key" ] && uci set $section.rustdesk_key="$key" - [ -n "$enabled" ] && uci set $section.rustdesk_enabled="$enabled" - uci commit system-hub + local host port device_id description ssl token + json_get_var host host + json_get_var port port + json_get_var device_id id + json_get_var description description + json_get_var ssl ssl + json_get_var token token - if [ -n "$server" ] || [ -n "$key" ]; then - mkdir -p /etc/rustdesk - cat > /etc/rustdesk/config.toml </dev/null 2>&1; then + uci add rtty rtty fi - if [ -x /etc/init.d/rustdesk ] && [ "${enabled:-0}" = "1" ]; then - /etc/init.d/rustdesk enable >/dev/null 2>&1 || true - /etc/init.d/rustdesk restart >/dev/null 2>&1 || true - elif [ -x /etc/init.d/rustdesk ]; then - /etc/init.d/rustdesk stop >/dev/null 2>&1 || true - /etc/init.d/rustdesk disable >/dev/null 2>&1 || true - fi + [ -n "$host" ] && uci set rtty.@rtty[0].host="$host" + [ -n "$port" ] && uci set rtty.@rtty[0].port="$port" + [ -n "$device_id" ] && uci set rtty.@rtty[0].id="$device_id" + [ -n "$description" ] && uci set rtty.@rtty[0].description="$description" + [ -n "$ssl" ] && uci set rtty.@rtty[0].ssl="$ssl" + [ -n "$token" ] && uci set rtty.@rtty[0].token="$token" + + uci commit rtty json_init json_add_boolean "success" 1 @@ -1325,19 +1330,19 @@ EOF } remote_get_credentials() { - local section="system-hub.remote" - local rid="" rpass="" - if command -v rustdesk >/dev/null 2>&1; then - rid=$(rustdesk --get-id 2>/dev/null || echo "") - rpass=$(rustdesk --password 2>/dev/null || echo "") + local device_id=$(uci -q get rtty.@rtty[0].id || echo "") + local token=$(uci -q get rtty.@rtty[0].token || echo "") + + # Auto-generate ID from MAC if not set + if [ -z "$device_id" ]; then + local iface=$(uci -q get rtty.@rtty[0].interface || echo "lan") + device_id=$(cat /sys/class/net/br-$iface/address 2>/dev/null | tr -d ':' || cat /sys/class/net/eth0/address 2>/dev/null | tr -d ':' || echo "") fi - [ -z "$rid" ] && rid=$(uci -q get $section.rustdesk_id || echo "") - [ -z "$rpass" ] && rpass=$(uci -q get $section.rustdesk_password || echo "") json_init json_add_boolean "success" 1 - json_add_string "id" "$rid" - json_add_string "password" "$rpass" + json_add_string "id" "$device_id" + json_add_string "token" "$token" json_dump } @@ -1347,21 +1352,87 @@ remote_service_action() { json_get_var action action json_init - if [ ! -x /etc/init.d/rustdesk ]; then + + if ! command -v rtty >/dev/null 2>&1; then json_add_boolean "success" 0 - json_add_string "error" "service_missing" + json_add_string "error" "not_installed" json_dump return fi case "$action" in - start|stop|restart|enable|disable) - if /etc/init.d/rustdesk "$action" >/dev/null 2>&1; then + start) + # Stop any existing instance + killall rtty 2>/dev/null || true + sleep 1 + + # Get config + local host=$(uci -q get rtty.@rtty[0].host) + local port=$(uci -q get rtty.@rtty[0].port || echo "5912") + local device_id=$(uci -q get rtty.@rtty[0].id) + local token=$(uci -q get rtty.@rtty[0].token) + local ssl=$(uci -q get rtty.@rtty[0].ssl || echo "0") + local description=$(uci -q get rtty.@rtty[0].description) + + if [ -z "$host" ]; then + json_add_boolean "success" 0 + json_add_string "error" "no_server_configured" + json_dump + return + fi + + # Build command + local cmd="rtty -h $host -p $port -a -D" + [ -n "$device_id" ] && cmd="$cmd -I $device_id" + [ -n "$token" ] && cmd="$cmd -t $token" + [ -n "$description" ] && cmd="$cmd -d \"$description\"" + [ "$ssl" = "1" ] && cmd="$cmd -s -x" + + eval $cmd + + sleep 2 + if pidof rtty >/dev/null 2>&1; then json_add_boolean "success" 1 - json_add_string "message" "$action" + json_add_string "message" "rtty started" else json_add_boolean "success" 0 - json_add_string "error" "action_failed" + json_add_string "error" "failed_to_start" + fi + ;; + stop) + if killall rtty 2>/dev/null; then + json_add_boolean "success" 1 + json_add_string "message" "rtty stopped" + else + json_add_boolean "success" 0 + json_add_string "error" "not_running" + fi + ;; + restart) + killall rtty 2>/dev/null || true + sleep 1 + # Recursive call to start + echo '{"action":"start"}' | remote_service_action + return + ;; + enable) + if [ -x /etc/init.d/rtty ]; then + /etc/init.d/rtty enable >/dev/null 2>&1 + json_add_boolean "success" 1 + json_add_string "message" "rtty enabled" + else + json_add_boolean "success" 0 + json_add_string "error" "init_script_missing" + fi + ;; + disable) + if [ -x /etc/init.d/rtty ]; then + /etc/init.d/rtty disable >/dev/null 2>&1 + json_add_boolean "success" 1 + json_add_string "message" "rtty disabled" + else + json_add_boolean "success" 0 + json_add_string "error" "init_script_missing" fi ;; *) @@ -1375,16 +1446,195 @@ remote_service_action() { remote_save_settings() { read input json_load "$input" - local section="system-hub.remote" - local allow require notify - json_get_var allow allow_unattended - json_get_var require require_approval - json_get_var notify notify_on_connect - [ -n "$allow" ] && uci set $section.allow_unattended="$allow" - [ -n "$require" ] && uci set $section.require_approval="$require" - [ -n "$notify" ] && uci set $section.notify_on_connect="$notify" - uci commit system-hub + local host port device_id description ssl token + json_get_var host host + json_get_var port port + json_get_var device_id id + json_get_var description description + json_get_var ssl ssl + json_get_var token token + + # Ensure rtty config section exists + if ! uci -q get rtty.@rtty[0] >/dev/null 2>&1; then + uci add rtty rtty + fi + + [ -n "$host" ] && uci set rtty.@rtty[0].host="$host" + [ -n "$port" ] && uci set rtty.@rtty[0].port="$port" + [ -n "$device_id" ] && uci set rtty.@rtty[0].id="$device_id" + [ -n "$description" ] && uci set rtty.@rtty[0].description="$description" + [ -n "$ssl" ] && uci set rtty.@rtty[0].ssl="$ssl" + [ -n "$token" ] && uci set rtty.@rtty[0].token="$token" + + uci commit rtty + + json_init + json_add_boolean "success" 1 + json_dump +} + +# ============================================ +# TTYD Web Console Functions +# ============================================ + +ttyd_status() { + local installed=0 + local running=0 + local enabled=0 + local port=7681 + local interface="lan" + + # Check if installed + command -v ttyd >/dev/null 2>&1 && installed=1 + + # Check if running + if pidof ttyd >/dev/null 2>&1; then + running=1 + fi + + # Check if enabled in init + if [ -x /etc/init.d/ttyd ] && [ -f /etc/rc.d/S*ttyd 2>/dev/null ]; then + enabled=1 + fi + + # Get port from UCI config + local uci_port=$(uci -q get ttyd.@ttyd[0].port) + [ -n "$uci_port" ] && port="$uci_port" + + # Get interface binding + local uci_interface=$(uci -q get ttyd.@ttyd[0].interface) + [ -n "$uci_interface" ] && interface="$uci_interface" + + json_init + json_add_boolean "installed" "$installed" + json_add_boolean "running" "$running" + json_add_boolean "enabled" "$enabled" + json_add_int "port" "$port" + json_add_string "interface" "$interface" + json_dump +} + +ttyd_install() { + json_init + + if command -v ttyd >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "already_installed" + json_dump + return + fi + + if ! command -v opkg >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "opkg_missing" + json_dump + return + fi + + opkg update >/tmp/ttyd-install.log 2>&1 + if opkg install ttyd >>/tmp/ttyd-install.log 2>&1; then + # Configure ttyd for LAN only by default + if [ ! -f /etc/config/ttyd ]; then + cat > /etc/config/ttyd <<'TTYDCONF' +config ttyd + option port '7681' + option interface 'lan' + option command '/bin/login' + option ipv6 '0' +TTYDCONF + fi + json_add_boolean "success" 1 + json_add_string "message" "ttyd installed successfully" + else + local err="$(tail -n 20 /tmp/ttyd-install.log 2>/dev/null)" + json_add_boolean "success" 0 + json_add_string "error" "${err:-install_failed}" + fi + json_dump +} + +ttyd_start() { + json_init + + if ! command -v ttyd >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "not_installed" + json_dump + return + fi + + # Stop any existing instance + killall ttyd 2>/dev/null || true + sleep 1 + + # Get config + local port=$(uci -q get ttyd.@ttyd[0].port || echo 7681) + local interface=$(uci -q get ttyd.@ttyd[0].interface || echo "lan") + + # Get interface IP + local bind_ip="" + if [ "$interface" != "" ] && [ "$interface" != "0.0.0.0" ]; then + bind_ip=$(ubus call network.interface.$interface status 2>/dev/null | jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null) + fi + + # Start ttyd + if [ -n "$bind_ip" ]; then + ttyd -p "$port" -i "$bind_ip" -W /bin/login & + else + ttyd -p "$port" -W /bin/login & + fi + + sleep 2 + + if pidof ttyd >/dev/null 2>&1; then + json_add_boolean "success" 1 + json_add_string "message" "ttyd started on port $port" + json_add_int "port" "$port" + else + json_add_boolean "success" 0 + json_add_string "error" "failed_to_start" + fi + json_dump +} + +ttyd_stop() { + json_init + + if killall ttyd 2>/dev/null; then + json_add_boolean "success" 1 + json_add_string "message" "ttyd stopped" + else + json_add_boolean "success" 0 + json_add_string "error" "not_running" + fi + json_dump +} + +ttyd_configure() { + read input + json_load "$input" + + local port interface + json_get_var port port + json_get_var interface interface + + # Ensure config exists + if [ ! -f /etc/config/ttyd ]; then + touch /etc/config/ttyd + uci set ttyd.main=ttyd + fi + + [ -n "$port" ] && uci set ttyd.@ttyd[0].port="$port" + [ -n "$interface" ] && uci set ttyd.@ttyd[0].interface="$interface" + uci commit ttyd + + # Restart if running + if pidof ttyd >/dev/null 2>&1; then + killall ttyd 2>/dev/null + sleep 1 + ttyd_start >/dev/null 2>&1 + fi json_init json_add_boolean "success" 1 @@ -1634,7 +1884,7 @@ is_service_running() { fi # Fallback: check process - pgrep -x "$svc" >/dev/null 2>&1 && return 0 + pidof "$svc" >/dev/null 2>&1 && return 0 return 1 } @@ -1906,7 +2156,12 @@ case "$1" in "allow_unattended": 0, "require_approval": 1, "notify_on_connect": 1 - } + }, + "ttyd_status": {}, + "ttyd_install": {}, + "ttyd_start": {}, + "ttyd_stop": {}, + "ttyd_configure": { "port": 7681, "interface": "lan" } } EOF ;; @@ -1942,6 +2197,11 @@ EOF remote_get_credentials) remote_get_credentials ;; remote_service_action) remote_service_action ;; remote_save_settings) remote_save_settings ;; + ttyd_status) ttyd_status ;; + ttyd_install) ttyd_install ;; + ttyd_start) ttyd_start ;; + ttyd_stop) ttyd_stop ;; + ttyd_configure) ttyd_configure ;; *) json_init json_add_boolean "success" 0 diff --git a/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json b/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json index bd86cafb..143eea52 100644 --- a/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json +++ b/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json @@ -20,7 +20,8 @@ "download_diagnostic", "run_diagnostic_test", "remote_status", - "remote_get_credentials" + "remote_get_credentials", + "ttyd_status" ], "luci.secubox": [ "modules", @@ -48,7 +49,11 @@ "remote_install", "remote_configure", "remote_service_action", - "remote_save_settings" + "remote_save_settings", + "ttyd_install", + "ttyd_start", + "ttyd_stop", + "ttyd_configure" ] } }