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 <noreply@anthropic.com>
This commit is contained in:
parent
b198bb754a
commit
1056026168
34
package/secubox/luci-app-exposure/Makefile
Normal file
34
package/secubox/luci-app-exposure/Makefile
Normal file
@ -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 <contact@secubox.dev>
|
||||||
|
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))
|
||||||
@ -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
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
});
|
||||||
@ -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
|
||||||
|
});
|
||||||
@ -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
|
||||||
|
});
|
||||||
@ -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
|
||||||
|
});
|
||||||
459
package/secubox/luci-app-exposure/root/usr/libexec/rpcd/luci.exposure
Executable file
459
package/secubox/luci-app-exposure/root/usr/libexec/rpcd/luci.exposure
Executable file
@ -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
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user