From 1056026168d437e43c618741932d2f2eba45b424 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Sun, 25 Jan 2026 15:49:29 +0100 Subject: [PATCH] feat(luci): Add LuCI interface for Service Exposure Manager 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 --- package/secubox/luci-app-exposure/Makefile | 34 ++ .../luci-static/resources/exposure/api.js | 81 ++++ .../resources/exposure/dashboard.css | 241 +++++++++ .../resources/view/exposure/overview.js | 152 ++++++ .../resources/view/exposure/services.js | 237 +++++++++ .../resources/view/exposure/ssl.js | 220 +++++++++ .../resources/view/exposure/tor.js | 199 ++++++++ .../root/usr/libexec/rpcd/luci.exposure | 459 ++++++++++++++++++ .../share/luci/menu.d/luci-app-exposure.json | 38 ++ .../share/rpcd/acl.d/luci-app-exposure.json | 17 + 10 files changed, 1678 insertions(+) create mode 100644 package/secubox/luci-app-exposure/Makefile create mode 100644 package/secubox/luci-app-exposure/htdocs/luci-static/resources/exposure/api.js create mode 100644 package/secubox/luci-app-exposure/htdocs/luci-static/resources/exposure/dashboard.css create mode 100644 package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/overview.js create mode 100644 package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/services.js create mode 100644 package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/ssl.js create mode 100644 package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/tor.js create mode 100755 package/secubox/luci-app-exposure/root/usr/libexec/rpcd/luci.exposure create mode 100644 package/secubox/luci-app-exposure/root/usr/share/luci/menu.d/luci-app-exposure.json create mode 100644 package/secubox/luci-app-exposure/root/usr/share/rpcd/acl.d/luci-app-exposure.json diff --git a/package/secubox/luci-app-exposure/Makefile b/package/secubox/luci-app-exposure/Makefile new file mode 100644 index 00000000..0ede9978 --- /dev/null +++ b/package/secubox/luci-app-exposure/Makefile @@ -0,0 +1,34 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-exposure +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=SecuBox Team +PKG_LICENSE:=MIT + +LUCI_TITLE:=LuCI SecuBox Service Exposure Manager +LUCI_DEPENDS:=+luci-base +secubox-app-exposure +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-exposure/install + $(INSTALL_DIR) $(1)/usr/libexec/rpcd + $(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.exposure $(1)/usr/libexec/rpcd/ + + $(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d + $(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-exposure.json $(1)/usr/share/rpcd/acl.d/ + + $(INSTALL_DIR) $(1)/usr/share/luci/menu.d + $(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-exposure.json $(1)/usr/share/luci/menu.d/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/view/exposure + $(INSTALL_DATA) ./htdocs/luci-static/resources/view/exposure/*.js $(1)/www/luci-static/resources/view/exposure/ + + $(INSTALL_DIR) $(1)/www/luci-static/resources/exposure + $(INSTALL_DATA) ./htdocs/luci-static/resources/exposure/*.js $(1)/www/luci-static/resources/exposure/ + $(INSTALL_DATA) ./htdocs/luci-static/resources/exposure/*.css $(1)/www/luci-static/resources/exposure/ +endef + +$(eval $(call BuildPackage,luci-app-exposure)) diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/exposure/api.js b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/exposure/api.js new file mode 100644 index 00000000..3a621dd1 --- /dev/null +++ b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/exposure/api.js @@ -0,0 +1,81 @@ +'use strict'; +'require rpc'; + +var callScan = rpc.declare({ + object: 'luci.exposure', + method: 'scan', + expect: { services: [] } +}); + +var callConflicts = rpc.declare({ + object: 'luci.exposure', + method: 'conflicts', + expect: { conflicts: [] } +}); + +var callStatus = rpc.declare({ + object: 'luci.exposure', + method: 'status' +}); + +var callTorList = rpc.declare({ + object: 'luci.exposure', + method: 'tor_list', + expect: { services: [] } +}); + +var callSslList = rpc.declare({ + object: 'luci.exposure', + method: 'ssl_list', + expect: { backends: [] } +}); + +var callGetConfig = rpc.declare({ + object: 'luci.exposure', + method: 'get_config', + expect: { known_services: [] } +}); + +var callFixPort = rpc.declare({ + object: 'luci.exposure', + method: 'fix_port', + params: ['service', 'port'] +}); + +var callTorAdd = rpc.declare({ + object: 'luci.exposure', + method: 'tor_add', + params: ['service', 'local_port', 'onion_port'] +}); + +var callTorRemove = rpc.declare({ + object: 'luci.exposure', + method: 'tor_remove', + params: ['service'] +}); + +var callSslAdd = rpc.declare({ + object: 'luci.exposure', + method: 'ssl_add', + params: ['service', 'domain', 'local_port'] +}); + +var callSslRemove = rpc.declare({ + object: 'luci.exposure', + method: 'ssl_remove', + params: ['service'] +}); + +return { + scan: callScan, + conflicts: callConflicts, + status: callStatus, + torList: callTorList, + sslList: callSslList, + getConfig: callGetConfig, + fixPort: callFixPort, + torAdd: callTorAdd, + torRemove: callTorRemove, + sslAdd: callSslAdd, + sslRemove: callSslRemove +}; diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/exposure/dashboard.css b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/exposure/dashboard.css new file mode 100644 index 00000000..bce30695 --- /dev/null +++ b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/exposure/dashboard.css @@ -0,0 +1,241 @@ +/* SecuBox Service Exposure Manager - Dashboard Styles */ + +.exposure-dashboard { + padding: 1rem; +} + +.exposure-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +.exposure-stat-card { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #0f3460; + border-radius: 12px; + padding: 1.5rem; + text-align: center; +} + +.exposure-stat-icon { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.exposure-stat-value { + font-size: 2rem; + font-weight: 700; + color: #00d4ff; +} + +.exposure-stat-label { + font-size: 0.85rem; + color: #8892b0; + margin-top: 0.25rem; +} + +.exposure-section { + background: #16213e; + border: 1px solid #0f3460; + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +.exposure-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid #0f3460; +} + +.exposure-section-title { + font-size: 1.1rem; + font-weight: 600; + color: #ccd6f6; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.exposure-section-title .icon { + font-size: 1.25rem; +} + +/* Service Table */ +.exposure-table { + width: 100%; + border-collapse: collapse; +} + +.exposure-table th, +.exposure-table td { + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid #0f3460; +} + +.exposure-table th { + color: #8892b0; + font-weight: 500; + font-size: 0.85rem; + text-transform: uppercase; +} + +.exposure-table td { + color: #ccd6f6; +} + +.exposure-table tr:hover { + background: rgba(0, 212, 255, 0.05); +} + +/* Status badges */ +.badge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; +} + +.badge-external { + background: rgba(0, 212, 255, 0.15); + color: #00d4ff; +} + +.badge-local { + background: rgba(255, 193, 7, 0.15); + color: #ffc107; +} + +.badge-tor { + background: rgba(116, 78, 182, 0.15); + color: #9b59b6; +} + +.badge-ssl { + background: rgba(46, 204, 113, 0.15); + color: #2ecc71; +} + +/* Onion address */ +.onion-address { + font-family: monospace; + font-size: 0.85rem; + color: #9b59b6; + word-break: break-all; +} + +/* Buttons */ +.btn-action { + padding: 0.5rem 1rem; + border-radius: 6px; + border: none; + cursor: pointer; + font-size: 0.85rem; + transition: all 0.2s; +} + +.btn-primary { + background: linear-gradient(135deg, #00d4ff, #0099cc); + color: #fff; +} + +.btn-primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 212, 255, 0.3); +} + +.btn-danger { + background: rgba(231, 76, 60, 0.2); + color: #e74c3c; + border: 1px solid #e74c3c; +} + +.btn-danger:hover { + background: #e74c3c; + color: #fff; +} + +.btn-tor { + background: rgba(155, 89, 182, 0.2); + color: #9b59b6; + border: 1px solid #9b59b6; +} + +.btn-ssl { + background: rgba(46, 204, 113, 0.2); + color: #2ecc71; + border: 1px solid #2ecc71; +} + +/* Forms */ +.exposure-form { + display: flex; + gap: 1rem; + align-items: flex-end; + flex-wrap: wrap; +} + +.exposure-form-group { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.exposure-form-group label { + font-size: 0.85rem; + color: #8892b0; +} + +.exposure-form-group input, +.exposure-form-group select { + padding: 0.5rem 0.75rem; + border-radius: 6px; + border: 1px solid #0f3460; + background: #1a1a2e; + color: #ccd6f6; + font-size: 0.9rem; +} + +.exposure-form-group input:focus, +.exposure-form-group select:focus { + outline: none; + border-color: #00d4ff; +} + +/* Empty state */ +.exposure-empty { + text-align: center; + padding: 2rem; + color: #8892b0; +} + +.exposure-empty .icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +/* Port conflict warning */ +.conflict-warning { + background: rgba(231, 76, 60, 0.1); + border: 1px solid #e74c3c; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; +} + +.conflict-warning-header { + display: flex; + align-items: center; + gap: 0.5rem; + color: #e74c3c; + font-weight: 600; + margin-bottom: 0.5rem; +} diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/overview.js b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/overview.js new file mode 100644 index 00000000..2154b1a2 --- /dev/null +++ b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/overview.js @@ -0,0 +1,152 @@ +'use strict'; +'require view'; +'require dom'; +'require ui'; +'require exposure/api as api'; + +return view.extend({ + load: function() { + return Promise.all([ + api.status(), + api.conflicts() + ]); + }, + + render: function(data) { + var status = data[0] || {}; + var conflicts = data[1] || []; + + var services = status.services || {}; + var tor = status.tor || {}; + var ssl = status.ssl || {}; + + // 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', {}, 'Service Exposure Manager'), + E('p', { 'style': 'color: #8892b0; margin-bottom: 1.5rem;' }, + 'Manage port conflicts, Tor hidden services, and HAProxy SSL backends'), + + // Stats cards + E('div', { 'class': 'exposure-stats' }, [ + E('div', { 'class': 'exposure-stat-card' }, [ + E('div', { 'class': 'exposure-stat-icon' }, '\ud83d\udd0c'), + E('div', { 'class': 'exposure-stat-value' }, String(services.total || 0)), + E('div', { 'class': 'exposure-stat-label' }, 'Total Services') + ]), + E('div', { 'class': 'exposure-stat-card' }, [ + E('div', { 'class': 'exposure-stat-icon' }, '\ud83c\udf10'), + E('div', { 'class': 'exposure-stat-value' }, String(services.external || 0)), + E('div', { 'class': 'exposure-stat-label' }, 'External (0.0.0.0)') + ]), + E('div', { 'class': 'exposure-stat-card' }, [ + E('div', { 'class': 'exposure-stat-icon' }, '\ud83e\uddc5'), + E('div', { 'class': 'exposure-stat-value' }, String(tor.count || 0)), + E('div', { 'class': 'exposure-stat-label' }, 'Tor Hidden Services') + ]), + E('div', { 'class': 'exposure-stat-card' }, [ + E('div', { 'class': 'exposure-stat-icon' }, '\ud83d\udd12'), + E('div', { 'class': 'exposure-stat-value' }, String(ssl.count || 0)), + E('div', { 'class': 'exposure-stat-label' }, 'SSL Backends') + ]) + ]), + + // Conflicts warning + conflicts.length > 0 ? E('div', { 'class': 'conflict-warning' }, [ + E('div', { 'class': 'conflict-warning-header' }, [ + '\u26a0\ufe0f Port Conflicts Detected' + ]), + E('ul', {}, + conflicts.map(function(c) { + return E('li', {}, + 'Port ' + c.port + ': ' + (c.services || []).join(', ') + ); + }) + ) + ]) : null, + + // Tor Hidden Services section + E('div', { 'class': 'exposure-section' }, [ + E('div', { 'class': 'exposure-section-header' }, [ + E('div', { 'class': 'exposure-section-title' }, [ + E('span', { 'class': 'icon' }, '\ud83e\uddc5'), + 'Tor Hidden Services' + ]), + E('a', { + 'href': L.url('admin/secubox/network/exposure/tor'), + 'class': 'btn-action btn-primary' + }, 'Manage') + ]), + (tor.services && tor.services.length > 0) ? + E('table', { 'class': 'exposure-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Service'), + E('th', {}, 'Onion Address') + ]) + ]), + E('tbody', {}, + tor.services.map(function(svc) { + return E('tr', {}, [ + E('td', {}, svc.service), + E('td', { 'class': 'onion-address' }, svc.onion) + ]); + }) + ) + ]) : + E('div', { 'class': 'exposure-empty' }, [ + E('div', { 'class': 'icon' }, '\ud83e\uddc5'), + E('p', {}, 'No Tor hidden services configured') + ]) + ]), + + // SSL Backends section + E('div', { 'class': 'exposure-section' }, [ + E('div', { 'class': 'exposure-section-header' }, [ + E('div', { 'class': 'exposure-section-title' }, [ + E('span', { 'class': 'icon' }, '\ud83d\udd12'), + 'HAProxy SSL Backends' + ]), + E('a', { + 'href': L.url('admin/secubox/network/exposure/ssl'), + 'class': 'btn-action btn-primary' + }, 'Manage') + ]), + (ssl.backends && ssl.backends.length > 0) ? + E('table', { 'class': 'exposure-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Service'), + E('th', {}, 'Domain') + ]) + ]), + E('tbody', {}, + ssl.backends.map(function(b) { + return E('tr', {}, [ + E('td', {}, b.service), + E('td', {}, b.domain) + ]); + }) + ) + ]) : + E('div', { 'class': 'exposure-empty' }, [ + E('div', { 'class': 'icon' }, '\ud83d\udd12'), + E('p', {}, 'No SSL backends configured') + ]) + ]) + ].filter(Boolean)); + + return view; + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/services.js b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/services.js new file mode 100644 index 00000000..b8fa1e9f --- /dev/null +++ b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/services.js @@ -0,0 +1,237 @@ +'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 +}); diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/ssl.js b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/ssl.js new file mode 100644 index 00000000..eda8af0d --- /dev/null +++ b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/ssl.js @@ -0,0 +1,220 @@ +'use strict'; +'require view'; +'require dom'; +'require ui'; +'require exposure/api as api'; + +return view.extend({ + load: function() { + return Promise.all([ + api.sslList(), + api.scan() + ]); + }, + + render: function(data) { + var sslBackends = data[0] || []; + var allServices = 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', {}, '\ud83d\udd12 HAProxy SSL Backends'), + E('p', { 'style': 'color: #8892b0; margin-bottom: 1.5rem;' }, + 'Configure HTTPS reverse proxy for your services'), + + // Add new backend form + E('div', { 'class': 'exposure-section' }, [ + E('div', { 'class': 'exposure-section-header' }, [ + E('div', { 'class': 'exposure-section-title' }, [ + E('span', { 'class': 'icon' }, '\u2795'), + 'Add SSL Backend' + ]) + ]), + E('div', { 'class': 'exposure-form' }, [ + E('div', { 'class': 'exposure-form-group' }, [ + E('label', {}, 'Service'), + E('select', { 'id': 'new-ssl-service' }, + [E('option', { 'value': '' }, '-- Select --')].concat( + allServices.filter(function(s) { return s.external; }).map(function(s) { + var name = s.name || s.process; + return E('option', { + 'value': s.process, + 'data-port': s.port, + 'data-name': name.toLowerCase().replace(/\s+/g, '') + }, name + ' (:' + s.port + ')'); + }) + ) + ) + ]), + E('div', { 'class': 'exposure-form-group' }, [ + E('label', {}, 'Domain'), + E('input', { + 'type': 'text', + 'id': 'new-ssl-domain', + 'placeholder': 'service.example.com' + }) + ]), + E('div', { 'class': 'exposure-form-group' }, [ + E('label', {}, 'Backend Port'), + E('input', { 'type': 'number', 'id': 'new-ssl-port', 'placeholder': '3000' }) + ]), + E('button', { + 'class': 'btn-action btn-primary', + 'click': ui.createHandlerFn(self, 'handleAdd') + }, 'Add Backend') + ]) + ]), + + // Info box + E('div', { + 'class': 'exposure-section', + 'style': 'background: rgba(0, 212, 255, 0.1); border-color: #00d4ff;' + }, [ + E('p', { 'style': 'margin: 0; color: #ccd6f6;' }, [ + E('strong', {}, '\u2139\ufe0f SSL Certificate: '), + 'After adding a backend, upload the SSL certificate to ', + E('code', {}, '/srv/lxc/haproxy/rootfs/etc/haproxy/certs/'), + '. The certificate file should be named ', + E('code', {}, 'domain.pem'), + ' and contain both the certificate and private key.' + ]) + ]), + + // Existing backends + E('div', { 'class': 'exposure-section' }, [ + E('div', { 'class': 'exposure-section-header' }, [ + E('div', { 'class': 'exposure-section-title' }, [ + E('span', { 'class': 'icon' }, '\ud83d\udd12'), + 'Active SSL Backends (' + sslBackends.length + ')' + ]), + E('button', { + 'class': 'btn-action btn-primary', + 'click': function() { location.reload(); } + }, 'Refresh') + ]), + + sslBackends.length > 0 ? + E('table', { 'class': 'exposure-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Service'), + E('th', {}, 'Domain'), + E('th', {}, 'Backend'), + E('th', {}, 'Actions') + ]) + ]), + E('tbody', {}, + sslBackends.map(function(b) { + return E('tr', {}, [ + E('td', { 'style': 'font-weight: 600;' }, b.service), + E('td', {}, [ + E('a', { + 'href': 'https://' + b.domain, + 'target': '_blank', + 'style': 'color: #00d4ff;' + }, b.domain), + E('span', { 'style': 'margin-left: 0.5rem;' }, '\ud83d\udd17') + ]), + E('td', { 'style': 'font-family: monospace;' }, b.backend || 'N/A'), + E('td', {}, [ + E('button', { + 'class': 'btn-action btn-danger', + 'click': ui.createHandlerFn(self, 'handleRemove', b.service) + }, 'Remove') + ]) + ]); + }) + ) + ]) : + E('div', { 'class': 'exposure-empty' }, [ + E('div', { 'class': 'icon' }, '\ud83d\udd12'), + E('p', {}, 'No SSL backends configured'), + E('p', { 'style': 'font-size: 0.85rem;' }, 'Select a service above to add HTTPS access') + ]) + ]) + ]); + + // Wire up service selector + setTimeout(function() { + var sel = document.getElementById('new-ssl-service'); + var portInput = document.getElementById('new-ssl-port'); + var domainInput = document.getElementById('new-ssl-domain'); + if (sel && portInput) { + sel.addEventListener('change', function() { + var opt = sel.options[sel.selectedIndex]; + portInput.value = opt.dataset.port || ''; + if (opt.dataset.name) { + domainInput.placeholder = opt.dataset.name + '.example.com'; + } + }); + } + }, 100); + + return view; + }, + + handleAdd: function(ev) { + var service = document.getElementById('new-ssl-service').value; + var domain = document.getElementById('new-ssl-domain').value; + var port = parseInt(document.getElementById('new-ssl-port').value); + + if (!service) { + ui.addNotification(null, E('p', {}, 'Please select a service'), 'warning'); + return; + } + + if (!domain) { + ui.addNotification(null, E('p', {}, 'Please enter a domain'), 'warning'); + return; + } + + if (!port) { + ui.addNotification(null, E('p', {}, 'Please specify the backend port'), 'warning'); + return; + } + + api.sslAdd(service, domain, port).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, [ + 'SSL backend configured for ', + E('strong', {}, domain), + E('br'), + 'Remember to upload the SSL certificate!' + ]), 'success'); + location.reload(); + } else { + ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'danger'); + }); + }, + + handleRemove: function(service, ev) { + if (!confirm('Remove SSL backend for ' + service + '?')) { + return; + } + + api.sslRemove(service).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, 'SSL backend removed'), 'success'); + location.reload(); + } else { + ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger'); + } + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/tor.js b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/tor.js new file mode 100644 index 00000000..1f8a3bde --- /dev/null +++ b/package/secubox/luci-app-exposure/htdocs/luci-static/resources/view/exposure/tor.js @@ -0,0 +1,199 @@ +'use strict'; +'require view'; +'require dom'; +'require ui'; +'require exposure/api as api'; + +return view.extend({ + load: function() { + return Promise.all([ + api.torList(), + api.scan() + ]); + }, + + render: function(data) { + var torServices = data[0] || []; + var allServices = 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', {}, '\ud83e\uddc5 Tor Hidden Services'), + E('p', { 'style': 'color: #8892b0; margin-bottom: 1.5rem;' }, + 'Expose services on the Tor network with .onion addresses'), + + // Add new service form + E('div', { 'class': 'exposure-section' }, [ + E('div', { 'class': 'exposure-section-header' }, [ + E('div', { 'class': 'exposure-section-title' }, [ + E('span', { 'class': 'icon' }, '\u2795'), + 'Add Hidden Service' + ]) + ]), + E('div', { 'class': 'exposure-form' }, [ + E('div', { 'class': 'exposure-form-group' }, [ + E('label', {}, 'Service'), + E('select', { 'id': 'new-tor-service' }, + [E('option', { 'value': '' }, '-- Select --')].concat( + allServices.filter(function(s) { return s.external; }).map(function(s) { + var name = s.name || s.process; + return E('option', { 'value': s.process, 'data-port': s.port }, + name + ' (:' + s.port + ')'); + }) + ) + ) + ]), + E('div', { 'class': 'exposure-form-group' }, [ + E('label', {}, 'Local Port'), + E('input', { 'type': 'number', 'id': 'new-tor-port', 'placeholder': '3000' }) + ]), + E('div', { 'class': 'exposure-form-group' }, [ + E('label', {}, 'Onion Port'), + E('input', { 'type': 'number', 'id': 'new-tor-onion-port', 'value': '80' }) + ]), + E('button', { + 'class': 'btn-action btn-primary', + 'click': ui.createHandlerFn(self, 'handleAdd') + }, 'Create .onion') + ]) + ]), + + // Existing services + E('div', { 'class': 'exposure-section' }, [ + E('div', { 'class': 'exposure-section-header' }, [ + E('div', { 'class': 'exposure-section-title' }, [ + E('span', { 'class': 'icon' }, '\ud83e\uddc5'), + 'Active Hidden Services (' + torServices.length + ')' + ]), + E('button', { + 'class': 'btn-action btn-primary', + 'click': function() { location.reload(); } + }, 'Refresh') + ]), + + torServices.length > 0 ? + E('table', { 'class': 'exposure-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'Service'), + E('th', {}, 'Onion Address'), + E('th', {}, 'Port'), + E('th', {}, 'Backend'), + E('th', {}, 'Actions') + ]) + ]), + E('tbody', {}, + torServices.map(function(svc) { + return E('tr', {}, [ + E('td', { 'style': 'font-weight: 600;' }, svc.service), + E('td', {}, [ + E('code', { 'class': 'onion-address' }, svc.onion), + E('button', { + 'class': 'btn-action', + 'style': 'margin-left: 0.5rem; padding: 0.25rem 0.5rem;', + 'click': function() { + navigator.clipboard.writeText(svc.onion); + ui.addNotification(null, E('p', {}, 'Copied to clipboard'), 'info'); + } + }, '\ud83d\udccb') + ]), + E('td', {}, svc.port || '80'), + E('td', { 'style': 'font-family: monospace;' }, svc.backend || 'N/A'), + E('td', {}, [ + E('button', { + 'class': 'btn-action btn-danger', + 'click': ui.createHandlerFn(self, 'handleRemove', svc.service) + }, 'Remove') + ]) + ]); + }) + ) + ]) : + E('div', { 'class': 'exposure-empty' }, [ + E('div', { 'class': 'icon' }, '\ud83e\uddc5'), + E('p', {}, 'No Tor hidden services configured'), + E('p', { 'style': 'font-size: 0.85rem;' }, 'Select a service above to create a .onion address') + ]) + ]) + ]); + + // Wire up service selector + setTimeout(function() { + var sel = document.getElementById('new-tor-service'); + var portInput = document.getElementById('new-tor-port'); + if (sel && portInput) { + sel.addEventListener('change', function() { + var opt = sel.options[sel.selectedIndex]; + portInput.value = opt.dataset.port || ''; + }); + } + }, 100); + + return view; + }, + + handleAdd: function(ev) { + var service = document.getElementById('new-tor-service').value; + var port = parseInt(document.getElementById('new-tor-port').value); + var onionPort = parseInt(document.getElementById('new-tor-onion-port').value) || 80; + + if (!service) { + ui.addNotification(null, E('p', {}, 'Please select a service'), 'warning'); + return; + } + + if (!port) { + ui.addNotification(null, E('p', {}, 'Please specify the local port'), 'warning'); + return; + } + + ui.showModal('Creating Hidden Service...', [ + E('p', { 'class': 'spinning' }, 'Please wait, generating .onion address (this may take a moment)...') + ]); + + api.torAdd(service, port, onionPort).then(function(res) { + ui.hideModal(); + if (res.success) { + ui.addNotification(null, E('p', {}, [ + 'Hidden service created! ', + E('br'), + E('code', { 'style': 'word-break: break-all;' }, res.onion || 'Refresh to see address') + ]), 'success'); + location.reload(); + } else { + ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'danger'); + }); + }, + + handleRemove: function(service, ev) { + if (!confirm('Remove hidden service for ' + service + '?\n\nThe .onion address will be permanently lost.')) { + return; + } + + api.torRemove(service).then(function(res) { + if (res.success) { + ui.addNotification(null, E('p', {}, 'Hidden service removed'), 'success'); + location.reload(); + } else { + ui.addNotification(null, E('p', {}, 'Error: ' + (res.error || 'Unknown error')), 'danger'); + } + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/package/secubox/luci-app-exposure/root/usr/libexec/rpcd/luci.exposure b/package/secubox/luci-app-exposure/root/usr/libexec/rpcd/luci.exposure new file mode 100755 index 00000000..203538af --- /dev/null +++ b/package/secubox/luci-app-exposure/root/usr/libexec/rpcd/luci.exposure @@ -0,0 +1,459 @@ +#!/bin/sh +# +# RPCD backend for SecuBox Service Exposure Manager +# + +. /usr/share/libubox/jshn.sh +. /lib/functions.sh + +case "$1" in + list) + json_init + json_add_object "scan" + json_close_object + json_add_object "conflicts" + json_close_object + json_add_object "status" + json_close_object + json_add_object "tor_list" + json_close_object + json_add_object "ssl_list" + json_close_object + json_add_object "get_config" + json_close_object + json_add_object "fix_port" + json_add_string "service" "string" + json_add_int "port" "integer" + json_close_object + json_add_object "tor_add" + json_add_string "service" "string" + json_add_int "local_port" "integer" + json_add_int "onion_port" "integer" + json_close_object + json_add_object "tor_remove" + json_add_string "service" "string" + json_close_object + json_add_object "ssl_add" + json_add_string "service" "string" + json_add_string "domain" "string" + json_add_int "local_port" "integer" + json_close_object + json_add_object "ssl_remove" + json_add_string "service" "string" + json_close_object + json_dump + ;; + + call) + case "$2" in + scan) + # Scan listening services + json_init + json_add_array "services" + + netstat -tlnp 2>/dev/null | grep LISTEN | awk '{ + split($4, a, ":") + port = a[length(a)] + if (!seen[port]++) { + split($7, p, "/") + proc = p[2] + if (proc == "") proc = "unknown" + print port, $4, proc + } + }' | sort -n | while read port addr proc; do + # Determine external status + external=0 + case "$addr" in + *0.0.0.0*|*::*) external=1 ;; + *127.0.0.1*|*::1*) external=0 ;; + *) external=1 ;; + esac + + # Get friendly name + name="$proc" + case "$proc" in + sshd|dropbear) name="SSH" ;; + dnsmasq) name="DNS" ;; + haproxy) name="HAProxy" ;; + uhttpd) name="LuCI" ;; + gitea) name="Gitea" ;; + netifyd) name="Netifyd" ;; + tor) name="Tor" ;; + python*) name="Python App" ;; + esac + + json_add_object "" + json_add_int "port" "$port" + json_add_string "address" "$addr" + json_add_string "process" "$proc" + json_add_string "name" "$name" + json_add_boolean "external" "$external" + json_close_object + done + + json_close_array + json_dump + ;; + + conflicts) + # Check for port conflicts + json_init + json_add_array "conflicts" + + config_load "secubox-exposure" + + TMP_PORTS="/tmp/exposure_ports_$$" + > "$TMP_PORTS" + + check_known() { + local section="$1" + local default_port config_path + config_get default_port "$section" default_port + config_get config_path "$section" config_path + + if [ -n "$config_path" ]; then + local actual_port=$(uci -q get "$config_path" 2>/dev/null) + [ -z "$actual_port" ] && actual_port="$default_port" + echo "$actual_port $section" >> "$TMP_PORTS" + fi + } + config_foreach check_known known + + # Find duplicates + sort "$TMP_PORTS" | uniq -d -w5 | while read port svc; do + json_add_object "" + json_add_int "port" "$port" + json_add_array "services" + grep "^$port " "$TMP_PORTS" | while read p s; do + json_add_string "" "$s" + done + json_close_array + json_close_object + done + + rm -f "$TMP_PORTS" + json_close_array + json_dump + ;; + + status) + # Get overall status + json_init + + # Count services + local total=$(netstat -tlnp 2>/dev/null | grep LISTEN | awk '{split($4,a,":"); print a[length(a)]}' | sort -u | wc -l) + local external=$(netstat -tlnp 2>/dev/null | grep LISTEN | grep -E "0\.0\.0\.0|::" | awk '{split($4,a,":"); print a[length(a)]}' | sort -u | wc -l) + + json_add_object "services" + json_add_int "total" "$total" + json_add_int "external" "$external" + json_close_object + + # Tor hidden services + config_load "secubox-exposure" + config_get TOR_DIR main tor_hidden_dir "/var/lib/tor/hidden_services" + + local tor_count=0 + [ -d "$TOR_DIR" ] && tor_count=$(ls -1d "$TOR_DIR"/*/ 2>/dev/null | wc -l) + + json_add_object "tor" + json_add_int "count" "$tor_count" + json_add_array "services" + if [ -d "$TOR_DIR" ]; then + for dir in "$TOR_DIR"/*/; do + [ -d "$dir" ] || continue + local svc=$(basename "$dir") + local onion="" + [ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname") + if [ -n "$onion" ]; then + json_add_object "" + json_add_string "service" "$svc" + json_add_string "onion" "$onion" + json_close_object + fi + done + fi + json_close_array + json_close_object + + # HAProxy SSL backends + config_get HAPROXY_CONFIG main haproxy_config "/srv/lxc/haproxy/rootfs/etc/haproxy/haproxy.cfg" + + local ssl_count=0 + [ -f "$HAPROXY_CONFIG" ] && ssl_count=$(grep -c "^backend.*_backend$" "$HAPROXY_CONFIG" 2>/dev/null || echo 0) + + json_add_object "ssl" + json_add_int "count" "$ssl_count" + json_add_array "backends" + if [ -f "$HAPROXY_CONFIG" ]; then + grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" | while read line; do + local backend=$(echo "$line" | awk '{print $2}' | sed 's/_backend$//') + local domain=$(grep "acl host_${backend} " "$HAPROXY_CONFIG" | awk '{print $NF}') + json_add_object "" + json_add_string "service" "$backend" + json_add_string "domain" "${domain:-N/A}" + json_close_object + done + fi + json_close_array + json_close_object + + json_dump + ;; + + tor_list) + # List Tor hidden services + json_init + json_add_array "services" + + config_load "secubox-exposure" + config_get TOR_DIR main tor_hidden_dir "/var/lib/tor/hidden_services" + config_get TOR_CONFIG main tor_config "/etc/tor/torrc" + + if [ -d "$TOR_DIR" ]; then + for dir in "$TOR_DIR"/*/; do + [ -d "$dir" ] || continue + local svc=$(basename "$dir") + local onion="" + [ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname") + + # Get port from torrc + local port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $2}') + local backend=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $3}') + + if [ -n "$onion" ]; then + json_add_object "" + json_add_string "service" "$svc" + json_add_string "onion" "$onion" + json_add_string "port" "${port:-80}" + json_add_string "backend" "${backend:-N/A}" + json_close_object + fi + done + fi + + json_close_array + json_dump + ;; + + ssl_list) + # List HAProxy SSL backends + json_init + json_add_array "backends" + + config_load "secubox-exposure" + config_get HAPROXY_CONFIG main haproxy_config "/srv/lxc/haproxy/rootfs/etc/haproxy/haproxy.cfg" + + if [ -f "$HAPROXY_CONFIG" ]; then + grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" | while read line; do + local backend=$(echo "$line" | awk '{print $2}') + local service=$(echo "$backend" | sed 's/_backend$//') + local domain=$(grep "acl host_${service} " "$HAPROXY_CONFIG" | awk '{print $NF}') + local server=$(grep -A5 "backend $backend" "$HAPROXY_CONFIG" | grep "server " | awk '{print $3}') + + json_add_object "" + json_add_string "service" "$service" + json_add_string "domain" "${domain:-N/A}" + json_add_string "backend" "${server:-N/A}" + json_close_object + done + fi + + json_close_array + json_dump + ;; + + get_config) + # Get known services configuration + json_init + json_add_array "known_services" + + config_load "secubox-exposure" + + get_known() { + local section="$1" + local default_port config_path category + + config_get default_port "$section" default_port + config_get config_path "$section" config_path + config_get category "$section" category "other" + + # Get actual configured port + local actual_port="" + if [ -n "$config_path" ]; then + actual_port=$(uci -q get "$config_path" 2>/dev/null) + fi + [ -z "$actual_port" ] && actual_port="$default_port" + + # Check if service is exposed + local tor_enabled ssl_enabled + config_get_bool tor_enabled "$section" tor 0 + config_get_bool ssl_enabled "$section" ssl 0 + local tor_onion ssl_domain + config_get tor_onion "$section" tor_onion + config_get ssl_domain "$section" ssl_domain + + json_add_object "" + json_add_string "id" "$section" + json_add_int "default_port" "$default_port" + json_add_int "actual_port" "$actual_port" + json_add_string "config_path" "$config_path" + json_add_string "category" "$category" + json_add_boolean "tor" "$tor_enabled" + [ -n "$tor_onion" ] && json_add_string "tor_onion" "$tor_onion" + json_add_boolean "ssl" "$ssl_enabled" + [ -n "$ssl_domain" ] && json_add_string "ssl_domain" "$ssl_domain" + json_close_object + } + config_foreach get_known known + + json_close_array + json_dump + ;; + + fix_port) + read -r input + service=$(echo "$input" | jsonfilter -e '@.service') + port=$(echo "$input" | jsonfilter -e '@.port') + + if [ -z "$service" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Service name required" + json_dump + exit 0 + fi + + result=$(/usr/sbin/secubox-exposure fix-port "$service" "$port" 2>&1) + if [ $? -eq 0 ]; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "$result" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "$result" + json_dump + fi + ;; + + tor_add) + read -r input + service=$(echo "$input" | jsonfilter -e '@.service') + local_port=$(echo "$input" | jsonfilter -e '@.local_port') + onion_port=$(echo "$input" | jsonfilter -e '@.onion_port') + + if [ -z "$service" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Service name required" + json_dump + exit 0 + fi + + result=$(/usr/sbin/secubox-exposure tor add "$service" "$local_port" "$onion_port" 2>&1) + if echo "$result" | grep -q "Hidden service created"; then + onion=$(echo "$result" | grep "Onion:" | awk '{print $2}') + json_init + json_add_boolean "success" 1 + json_add_string "onion" "$onion" + json_add_string "message" "Hidden service created" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "$result" + json_dump + fi + ;; + + tor_remove) + read -r input + service=$(echo "$input" | jsonfilter -e '@.service') + + if [ -z "$service" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Service name required" + json_dump + exit 0 + fi + + result=$(/usr/sbin/secubox-exposure tor remove "$service" 2>&1) + if echo "$result" | grep -q "removed"; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "Hidden service removed" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "$result" + json_dump + fi + ;; + + ssl_add) + read -r input + service=$(echo "$input" | jsonfilter -e '@.service') + domain=$(echo "$input" | jsonfilter -e '@.domain') + local_port=$(echo "$input" | jsonfilter -e '@.local_port') + + if [ -z "$service" ] || [ -z "$domain" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Service and domain required" + json_dump + exit 0 + fi + + result=$(/usr/sbin/secubox-exposure ssl add "$service" "$domain" "$local_port" 2>&1) + if echo "$result" | grep -q "configured"; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "SSL backend configured" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "$result" + json_dump + fi + ;; + + ssl_remove) + read -r input + service=$(echo "$input" | jsonfilter -e '@.service') + + if [ -z "$service" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "Service name required" + json_dump + exit 0 + fi + + result=$(/usr/sbin/secubox-exposure ssl remove "$service" 2>&1) + if echo "$result" | grep -q "removed"; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "SSL backend removed" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "$result" + json_dump + fi + ;; + + *) + json_init + json_add_boolean "error" 1 + json_add_string "message" "Unknown method: $2" + json_dump + ;; + esac + ;; +esac diff --git a/package/secubox/luci-app-exposure/root/usr/share/luci/menu.d/luci-app-exposure.json b/package/secubox/luci-app-exposure/root/usr/share/luci/menu.d/luci-app-exposure.json new file mode 100644 index 00000000..93fc18e2 --- /dev/null +++ b/package/secubox/luci-app-exposure/root/usr/share/luci/menu.d/luci-app-exposure.json @@ -0,0 +1,38 @@ +{ + "admin/secubox/network/exposure": { + "title": "Service Exposure", + "order": 35, + "action": { + "type": "view", + "path": "exposure/overview" + }, + "depends": { + "acl": ["luci-app-exposure"], + "uci": { "secubox-exposure": true } + } + }, + "admin/secubox/network/exposure/services": { + "title": "Services", + "order": 1, + "action": { + "type": "view", + "path": "exposure/services" + } + }, + "admin/secubox/network/exposure/tor": { + "title": "Tor Hidden", + "order": 2, + "action": { + "type": "view", + "path": "exposure/tor" + } + }, + "admin/secubox/network/exposure/ssl": { + "title": "SSL Proxy", + "order": 3, + "action": { + "type": "view", + "path": "exposure/ssl" + } + } +} diff --git a/package/secubox/luci-app-exposure/root/usr/share/rpcd/acl.d/luci-app-exposure.json b/package/secubox/luci-app-exposure/root/usr/share/rpcd/acl.d/luci-app-exposure.json new file mode 100644 index 00000000..8825e107 --- /dev/null +++ b/package/secubox/luci-app-exposure/root/usr/share/rpcd/acl.d/luci-app-exposure.json @@ -0,0 +1,17 @@ +{ + "luci-app-exposure": { + "description": "Grant access to SecuBox Service Exposure Manager", + "read": { + "ubus": { + "luci.exposure": ["scan", "conflicts", "status", "tor_list", "ssl_list", "get_config"] + }, + "uci": ["secubox-exposure"] + }, + "write": { + "ubus": { + "luci.exposure": ["fix_port", "tor_add", "tor_remove", "ssl_add", "ssl_remove", "set_config"] + }, + "uci": ["secubox-exposure"] + } + } +}