secubox-openwrt/package/secubox/luci-app-webradio/htdocs/luci-static/resources/view/webradio/security.js
CyberMind-FR 418e99e481 feat(webradio): Add luci-app-webradio LuCI interface
Complete WebRadio management interface for OpenWrt:
- Dashboard with server status, listeners, now playing
- Icecast/Ezstream server configuration
- Playlist management with shuffle/upload
- Programming grid scheduler with jingle support
- Live audio input via DarkIce (ALSA)
- Security: SSL/TLS, rate limiting, CrowdSec integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-17 14:25:31 +01:00

354 lines
11 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require ui';
'require uci';
'require form';
var callSecurityStatus = rpc.declare({
object: 'luci.webradio',
method: 'security_status',
expect: {}
});
var callInstallCrowdsec = rpc.declare({
object: 'luci.webradio',
method: 'install_crowdsec',
expect: {}
});
var callGenerateCert = rpc.declare({
object: 'luci.webradio',
method: 'generate_ssl_cert',
params: ['hostname'],
expect: {}
});
return view.extend({
load: function() {
return Promise.all([
callSecurityStatus(),
uci.load('icecast')
]);
},
render: function(data) {
var self = this;
var status = data[0] || {};
var content = [
E('h2', {}, 'Security & Hardening'),
// SSL/TLS Section
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'SSL/TLS Encryption'),
E('p', { 'style': 'color: #666;' },
'Enable HTTPS for secure streaming. Listeners can connect via https://hostname:8443/live'),
E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'width: 180px;' }, 'SSL Status'),
E('div', { 'class': 'td' },
status.ssl_enabled
? this.statusBadge(true, 'Enabled')
: this.statusBadge(false, 'Disabled'))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Certificate'),
E('div', { 'class': 'td' },
status.ssl_cert_exists
? E('span', { 'style': 'color: green;' }, 'Found: ' + status.ssl_cert_path)
: E('span', { 'style': 'color: orange;' }, 'Not found'))
]),
status.ssl_cert_expiry ? E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Expires'),
E('div', { 'class': 'td' }, status.ssl_cert_expiry)
]) : ''
]),
E('div', { 'class': 'cbi-value', 'style': 'margin-top: 15px;' }, [
E('label', { 'class': 'cbi-value-title' }, 'Enable SSL'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'checkbox',
'id': 'ssl-enabled',
'checked': uci.get('icecast', 'ssl', 'enabled') === '1'
}),
E('span', { 'style': 'margin-left: 10px;' },
'Enable HTTPS streaming on port 8443')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'SSL Port'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'ssl-port',
'class': 'cbi-input-text',
'value': uci.get('icecast', 'ssl', 'port') || '8443',
'style': 'width: 100px;'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Certificate Path'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'ssl-cert',
'class': 'cbi-input-text',
'value': uci.get('icecast', 'ssl', 'certificate') || '/etc/ssl/certs/icecast.pem',
'style': 'width: 300px;'
})
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Private Key Path'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'text',
'id': 'ssl-key',
'class': 'cbi-input-text',
'value': uci.get('icecast', 'ssl', 'key') || '/etc/ssl/private/icecast.key',
'style': 'width: 300px;'
})
])
]),
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 15px;' }, [
E('button', {
'class': 'btn cbi-button-action',
'click': ui.createHandlerFn(this, 'handleSaveSSL')
}, 'Save SSL Settings'),
E('button', {
'class': 'btn cbi-button-neutral',
'click': ui.createHandlerFn(this, 'handleGenerateCert')
}, 'Generate Self-Signed Certificate')
])
]),
// Rate Limiting Section
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Rate Limiting'),
E('p', { 'style': 'color: #666;' },
'Configure connection limits to prevent abuse'),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Client Timeout'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'client-timeout',
'class': 'cbi-input-text',
'value': uci.get('icecast', 'ratelimit', 'client_timeout') || '30',
'style': 'width: 100px;'
}),
E('span', { 'style': 'margin-left: 10px; color: #666;' }, 'seconds')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Burst Size'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'burst-size',
'class': 'cbi-input-text',
'value': uci.get('icecast', 'ratelimit', 'burst_size') || '65535',
'style': 'width: 120px;'
}),
E('span', { 'style': 'margin-left: 10px; color: #666;' }, 'bytes')
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Queue Size'),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'type': 'number',
'id': 'queue-size',
'class': 'cbi-input-text',
'value': uci.get('icecast', 'ratelimit', 'queue_size') || '524288',
'style': 'width: 120px;'
}),
E('span', { 'style': 'margin-left: 10px; color: #666;' }, 'bytes')
])
]),
E('button', {
'class': 'btn cbi-button-action',
'style': 'margin-top: 10px;',
'click': ui.createHandlerFn(this, 'handleSaveRateLimit')
}, 'Save Rate Limits')
]),
// CrowdSec Section
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'CrowdSec Integration'),
E('p', { 'style': 'color: #666;' },
'Automatic abuse detection and IP blocking with CrowdSec'),
E('div', { 'class': 'table' }, [
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td', 'style': 'width: 180px;' }, 'CrowdSec'),
E('div', { 'class': 'td' },
status.crowdsec_installed
? this.statusBadge(true, 'Installed')
: this.statusBadge(false, 'Not Installed'))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Icecast Parsers'),
E('div', { 'class': 'td' },
status.crowdsec_parsers
? this.statusBadge(true, 'Installed')
: this.statusBadge(false, 'Not Installed'))
]),
E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Icecast Scenarios'),
E('div', { 'class': 'td' },
status.crowdsec_scenarios
? this.statusBadge(true, 'Installed')
: this.statusBadge(false, 'Not Installed'))
]),
status.crowdsec_decisions ? E('div', { 'class': 'tr' }, [
E('div', { 'class': 'td' }, 'Active Bans'),
E('div', { 'class': 'td' }, String(status.crowdsec_decisions))
]) : ''
]),
E('div', { 'style': 'margin-top: 15px;' }, [
E('p', {}, 'CrowdSec protection includes:'),
E('ul', { 'style': 'color: #666;' }, [
E('li', {}, 'Connection flood detection (20+ connections in 30s)'),
E('li', {}, 'Bandwidth abuse / stream ripping detection'),
E('li', {}, 'Automatic IP blocking via firewall bouncer')
])
]),
status.crowdsec_installed ? E('button', {
'class': 'btn cbi-button-positive',
'style': 'margin-top: 10px;',
'click': ui.createHandlerFn(this, 'handleInstallCrowdsec')
}, status.crowdsec_parsers ? 'Reinstall CrowdSec Rules' : 'Install CrowdSec Rules')
: E('p', { 'style': 'color: orange; margin-top: 10px;' },
'Install CrowdSec package first: opkg install crowdsec crowdsec-firewall-bouncer')
]),
// Security Tips
E('div', { 'class': 'cbi-section' }, [
E('h3', {}, 'Security Tips'),
E('ul', { 'style': 'color: #666;' }, [
E('li', {}, 'Change default passwords immediately (admin, source, relay)'),
E('li', {}, 'Use SSL/TLS for all public-facing streams'),
E('li', {}, 'Enable CrowdSec to automatically block abusive IPs'),
E('li', {}, 'Set reasonable listener limits to prevent resource exhaustion'),
E('li', {}, 'Monitor logs regularly: /var/log/icecast/'),
E('li', {}, 'Consider using firewall rules to restrict source connections to localhost')
])
])
];
return E('div', { 'class': 'cbi-map' }, content);
},
statusBadge: function(ok, text) {
var style = ok
? 'color: #fff; background: #5cb85c; padding: 2px 8px; border-radius: 3px;'
: 'color: #fff; background: #d9534f; padding: 2px 8px; border-radius: 3px;';
return E('span', { 'style': style }, text);
},
handleSaveSSL: function() {
var enabled = document.getElementById('ssl-enabled').checked;
var port = document.getElementById('ssl-port').value;
var cert = document.getElementById('ssl-cert').value;
var key = document.getElementById('ssl-key').value;
uci.set('icecast', 'ssl', 'ssl');
uci.set('icecast', 'ssl', 'enabled', enabled ? '1' : '0');
uci.set('icecast', 'ssl', 'port', port);
uci.set('icecast', 'ssl', 'certificate', cert);
uci.set('icecast', 'ssl', 'key', key);
return uci.save().then(function() {
return uci.apply();
}).then(function() {
ui.addNotification(null, E('p', 'SSL settings saved. Restart Icecast to apply.'));
});
},
handleSaveRateLimit: function() {
var clientTimeout = document.getElementById('client-timeout').value;
var burstSize = document.getElementById('burst-size').value;
var queueSize = document.getElementById('queue-size').value;
uci.set('icecast', 'ratelimit', 'ratelimit');
uci.set('icecast', 'ratelimit', 'client_timeout', clientTimeout);
uci.set('icecast', 'ratelimit', 'burst_size', burstSize);
uci.set('icecast', 'ratelimit', 'queue_size', queueSize);
return uci.save().then(function() {
return uci.apply();
}).then(function() {
ui.addNotification(null, E('p', 'Rate limit settings saved. Restart Icecast to apply.'));
});
},
handleGenerateCert: function() {
var hostname = uci.get('icecast', 'server', 'hostname') || 'localhost';
ui.showModal('Generate Certificate', [
E('p', {}, 'Generate a self-signed SSL certificate for: ' + hostname),
E('p', { 'style': 'color: orange;' },
'Note: Self-signed certificates will show browser warnings. For production, use Let\'s Encrypt or a proper CA.'),
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 15px;' }, [
E('button', {
'class': 'btn cbi-button-positive',
'click': L.bind(function() {
ui.hideModal();
ui.showModal('Generating', [
E('p', { 'class': 'spinning' }, 'Generating certificate...')
]);
callGenerateCert(hostname).then(function(res) {
ui.hideModal();
if (res.result === 'ok') {
ui.addNotification(null, E('p', 'Certificate generated successfully'));
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
}
});
}, this)
}, 'Generate'),
E('button', {
'class': 'btn',
'click': function() { ui.hideModal(); }
}, 'Cancel')
])
]);
},
handleInstallCrowdsec: function() {
ui.showModal('Installing CrowdSec Rules', [
E('p', { 'class': 'spinning' }, 'Installing Icecast parsers and scenarios...')
]);
return callInstallCrowdsec().then(function(res) {
ui.hideModal();
if (res.result === 'ok') {
ui.addNotification(null, E('p', 'CrowdSec rules installed successfully'));
window.location.reload();
} else {
ui.addNotification(null, E('p', 'Failed: ' + (res.error || 'unknown')), 'error');
}
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});