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 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-09 13:13:21 +01:00
parent 50bd0c872e
commit fa02d44f8d
4 changed files with 719 additions and 165 deletions

View File

@ -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);
}
});

View File

@ -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

View File

@ -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 <<EOF
[relay]
server = "${server:-$(uci -q get $section.rustdesk_server)}"
[options]
key = "${key:-$(uci -q get $section.rustdesk_key)}"
auto_start = true
EOF
# Ensure rtty config section exists
if ! uci -q get rtty.@rtty[0] >/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

View File

@ -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"
]
}
}