Complete LuCI app with: - Overview dashboard with stats (services, Tor, SSL counts) - Port conflict detection and warnings - Services list with quick actions - Tor hidden services management (add/list/remove) - HAProxy SSL backends management (add/list/remove) Views: overview.js, services.js, tor.js, ssl.js RPCD: luci.exposure backend Menu: admin/secubox/network/exposure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
238 lines
10 KiB
JavaScript
238 lines
10 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require ui';
|
|
'require exposure/api as api';
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
api.scan(),
|
|
api.getConfig()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var services = data[0] || [];
|
|
var config = data[1] || [];
|
|
var self = this;
|
|
|
|
// Inject CSS
|
|
var cssLink = document.querySelector('link[href*="exposure/dashboard.css"]');
|
|
if (!cssLink) {
|
|
var link = document.createElement('link');
|
|
link.rel = 'stylesheet';
|
|
link.href = L.resource('exposure/dashboard.css');
|
|
document.head.appendChild(link);
|
|
}
|
|
|
|
var view = E('div', { 'class': 'exposure-dashboard' }, [
|
|
E('h2', {}, 'Listening Services'),
|
|
E('p', { 'style': 'color: #8892b0; margin-bottom: 1.5rem;' },
|
|
'All services currently listening on network ports'),
|
|
|
|
E('div', { 'class': 'exposure-section' }, [
|
|
E('div', { 'class': 'exposure-section-header' }, [
|
|
E('div', { 'class': 'exposure-section-title' }, [
|
|
E('span', { 'class': 'icon' }, '\ud83d\udd0c'),
|
|
'Active Services (' + services.length + ')'
|
|
]),
|
|
E('button', {
|
|
'class': 'btn-action btn-primary',
|
|
'click': function() { location.reload(); }
|
|
}, 'Refresh')
|
|
]),
|
|
|
|
services.length > 0 ?
|
|
E('table', { 'class': 'exposure-table' }, [
|
|
E('thead', {}, [
|
|
E('tr', {}, [
|
|
E('th', {}, 'Port'),
|
|
E('th', {}, 'Service'),
|
|
E('th', {}, 'Process'),
|
|
E('th', {}, 'Address'),
|
|
E('th', {}, 'Status'),
|
|
E('th', {}, 'Actions')
|
|
])
|
|
]),
|
|
E('tbody', {},
|
|
services.map(function(svc) {
|
|
var isKnown = config.find(function(k) {
|
|
return k.actual_port == svc.port;
|
|
});
|
|
|
|
return E('tr', {}, [
|
|
E('td', { 'style': 'font-weight: 600;' }, String(svc.port)),
|
|
E('td', {}, svc.name || svc.process),
|
|
E('td', { 'style': 'font-family: monospace; color: #8892b0;' }, svc.process),
|
|
E('td', { 'style': 'font-family: monospace;' }, svc.address),
|
|
E('td', {}, [
|
|
E('span', {
|
|
'class': 'badge ' + (svc.external ? 'badge-external' : 'badge-local')
|
|
}, svc.external ? 'External' : 'Local')
|
|
]),
|
|
E('td', {}, [
|
|
svc.external ? E('button', {
|
|
'class': 'btn-action btn-tor',
|
|
'style': 'margin-right: 0.5rem;',
|
|
'click': ui.createHandlerFn(self, 'handleAddTor', svc)
|
|
}, '\ud83e\uddc5 Tor') : null,
|
|
svc.external ? E('button', {
|
|
'class': 'btn-action btn-ssl',
|
|
'click': ui.createHandlerFn(self, 'handleAddSsl', svc)
|
|
}, '\ud83d\udd12 SSL') : null
|
|
].filter(Boolean))
|
|
]);
|
|
})
|
|
)
|
|
]) :
|
|
E('div', { 'class': 'exposure-empty' }, [
|
|
E('div', { 'class': 'icon' }, '\ud83d\udd0c'),
|
|
E('p', {}, 'No listening services detected')
|
|
])
|
|
])
|
|
]);
|
|
|
|
return view;
|
|
},
|
|
|
|
handleAddTor: function(svc, ev) {
|
|
var self = this;
|
|
var serviceName = svc.name ? svc.name.toLowerCase().replace(/\s+/g, '') : svc.process;
|
|
|
|
ui.showModal('Add Tor Hidden Service', [
|
|
E('p', {}, 'Create a .onion address for ' + (svc.name || svc.process)),
|
|
E('div', { 'class': 'exposure-form', 'style': 'flex-direction: column; align-items: stretch;' }, [
|
|
E('div', { 'class': 'exposure-form-group' }, [
|
|
E('label', {}, 'Service Name'),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'tor-service-name',
|
|
'value': serviceName,
|
|
'style': 'width: 100%;'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'exposure-form-group' }, [
|
|
E('label', {}, 'Local Port'),
|
|
E('input', {
|
|
'type': 'number',
|
|
'id': 'tor-local-port',
|
|
'value': svc.port,
|
|
'style': 'width: 100%;'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'exposure-form-group' }, [
|
|
E('label', {}, 'Onion Port (public)'),
|
|
E('input', {
|
|
'type': 'number',
|
|
'id': 'tor-onion-port',
|
|
'value': '80',
|
|
'style': 'width: 100%;'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1rem;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
var name = document.getElementById('tor-service-name').value;
|
|
var localPort = parseInt(document.getElementById('tor-local-port').value);
|
|
var onionPort = parseInt(document.getElementById('tor-onion-port').value);
|
|
|
|
ui.hideModal();
|
|
ui.showModal('Creating Hidden Service...', [
|
|
E('p', { 'class': 'spinning' }, 'Please wait, generating .onion address...')
|
|
]);
|
|
|
|
api.torAdd(name, localPort, onionPort).then(function(res) {
|
|
ui.hideModal();
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', {}, [
|
|
'Hidden service created! ',
|
|
E('br'),
|
|
E('code', {}, res.onion || 'Check Tor tab for address')
|
|
]), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger');
|
|
}
|
|
});
|
|
}
|
|
}, 'Create')
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleAddSsl: function(svc, ev) {
|
|
var serviceName = svc.name ? svc.name.toLowerCase().replace(/\s+/g, '') : svc.process;
|
|
|
|
ui.showModal('Add SSL Backend', [
|
|
E('p', {}, 'Configure HAProxy SSL reverse proxy for ' + (svc.name || svc.process)),
|
|
E('div', { 'class': 'exposure-form', 'style': 'flex-direction: column; align-items: stretch;' }, [
|
|
E('div', { 'class': 'exposure-form-group' }, [
|
|
E('label', {}, 'Service Name'),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'ssl-service-name',
|
|
'value': serviceName,
|
|
'style': 'width: 100%;'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'exposure-form-group' }, [
|
|
E('label', {}, 'Domain'),
|
|
E('input', {
|
|
'type': 'text',
|
|
'id': 'ssl-domain',
|
|
'placeholder': serviceName + '.example.com',
|
|
'style': 'width: 100%;'
|
|
})
|
|
]),
|
|
E('div', { 'class': 'exposure-form-group' }, [
|
|
E('label', {}, 'Backend Port'),
|
|
E('input', {
|
|
'type': 'number',
|
|
'id': 'ssl-port',
|
|
'value': svc.port,
|
|
'style': 'width: 100%;'
|
|
})
|
|
])
|
|
]),
|
|
E('div', { 'class': 'right', 'style': 'margin-top: 1rem;' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, 'Cancel'),
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': function() {
|
|
var name = document.getElementById('ssl-service-name').value;
|
|
var domain = document.getElementById('ssl-domain').value;
|
|
var port = parseInt(document.getElementById('ssl-port').value);
|
|
|
|
if (!domain) {
|
|
ui.addNotification(null, E('p', {}, 'Domain is required'), 'danger');
|
|
return;
|
|
}
|
|
|
|
ui.hideModal();
|
|
api.sslAdd(name, domain, port).then(function(res) {
|
|
if (res.success) {
|
|
ui.addNotification(null, E('p', {}, 'SSL backend configured for ' + domain), 'success');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger');
|
|
}
|
|
});
|
|
}
|
|
}, 'Configure')
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|