feat(routes-status): Add LuCI dashboard for HAProxy vhosts and mitmproxy routes
New package luci-app-routes-status providing: - HAProxy vhosts status overview (218+ vhosts supported) - mitmproxy route configuration status (OUT/IN routes) - SSL certificate status indicators - WAF bypass detection (vhosts not using mitmproxy_inspector) - Sync routes and add missing route actions - RPCD backend with batch processing for large vhost counts Accessible at Status → Routes Status in LuCI. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2bb40d9419
commit
eb9adfd06a
@ -4382,3 +4382,25 @@ git checkout HEAD -- index.html
|
|||||||
- `docs/sbom-pipeline.md` - Architecture, usage, CRA mapping
|
- `docs/sbom-pipeline.md` - Architecture, usage, CRA mapping
|
||||||
- `SECURITY.md` - CRA Art. 13 §6 compliant disclosure policy
|
- `SECURITY.md` - CRA Art. 13 §6 compliant disclosure policy
|
||||||
- VEX policy reference
|
- VEX policy reference
|
||||||
|
|
||||||
|
73. **Routes Status Dashboard & User Services (2026-03-04)**
|
||||||
|
- **New Package: `luci-app-routes-status`**
|
||||||
|
- HAProxy vhosts status dashboard
|
||||||
|
- mitmproxy route configuration status (OUT/IN routes)
|
||||||
|
- SSL certificate status indicators
|
||||||
|
- WAF bypass detection (vhosts not using mitmproxy_inspector)
|
||||||
|
- Route sync and add missing route actions
|
||||||
|
- RPCD backend with optimized batch processing for 200+ vhosts
|
||||||
|
- **Shell Scripting Fixes:**
|
||||||
|
- `luci.network-modes`: Removed 60 lines of orphan dead code after `esac`
|
||||||
|
- `luci.netdata-dashboard`: Fixed bash process substitution to POSIX awk
|
||||||
|
- **Service User Extensions:**
|
||||||
|
- `secubox-core-users`: Added gitea and jellyfin service support
|
||||||
|
- `luci-app-secubox-users`: Added Jellyfin checkbox in frontend
|
||||||
|
- **LXC Networking Fix:**
|
||||||
|
- Discovered: LXC containers can't reach host's `127.0.0.1`
|
||||||
|
- Fixed mitmproxy routes in nextcloudctl, metablogizerctl, mitmproxyctl
|
||||||
|
- Use host LAN IP (192.168.255.1) instead of localhost for route targets
|
||||||
|
- **Mailserver Recovery:**
|
||||||
|
- Fixed webmail.gk2.secubox.in login error by starting mailserver container
|
||||||
|
- Verified IMAP/SMTP connectivity (ports 143, 993, 25, 587, 465)
|
||||||
|
|||||||
31
package/secubox/luci-app-routes-status/Makefile
Normal file
31
package/secubox/luci-app-routes-status/Makefile
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=luci-app-routes-status
|
||||||
|
PKG_VERSION:=1.0.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_MAINTAINER:=SecuBox Team
|
||||||
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
|
|
||||||
|
LUCI_TITLE:=LuCI Routes Status Dashboard
|
||||||
|
LUCI_DESCRIPTION:=Dashboard showing HAProxy vhosts and mitmproxy route status
|
||||||
|
LUCI_DEPENDS:=+luci-base +secubox-app-haproxy +secubox-app-mitmproxy
|
||||||
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|
||||||
|
define Package/$(PKG_NAME)/install
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.routes-status $(1)/usr/libexec/rpcd/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||||
|
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-routes-status.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-routes-status.json $(1)/usr/share/luci/menu.d/
|
||||||
|
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/routes-status
|
||||||
|
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/routes-status/*.js $(1)/www/luci-static/resources/view/routes-status/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||||
@ -0,0 +1,271 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require dom';
|
||||||
|
'require ui';
|
||||||
|
'require rpc';
|
||||||
|
|
||||||
|
var callStatus = rpc.declare({
|
||||||
|
object: 'luci.routes-status',
|
||||||
|
method: 'status',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSyncRoutes = rpc.declare({
|
||||||
|
object: 'luci.routes-status',
|
||||||
|
method: 'sync_routes',
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callAddRoute = rpc.declare({
|
||||||
|
object: 'luci.routes-status',
|
||||||
|
method: 'add_route',
|
||||||
|
params: ['domain', 'port'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return callStatus();
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStatusBadge: function(running, label) {
|
||||||
|
var color = running ? '#4CAF50' : '#f44336';
|
||||||
|
return E('span', {
|
||||||
|
'style': 'display:inline-block;padding:4px 12px;margin:4px;border-radius:4px;color:#fff;background:' + color + ';font-size:0.9em;font-weight:500;'
|
||||||
|
}, label + ': ' + (running ? 'Running' : 'Stopped'));
|
||||||
|
},
|
||||||
|
|
||||||
|
renderRouteBadge: function(hasRoute, type) {
|
||||||
|
if (hasRoute) {
|
||||||
|
return E('span', {
|
||||||
|
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:#4CAF50;font-size:0.8em;'
|
||||||
|
}, type);
|
||||||
|
} else {
|
||||||
|
return E('span', {
|
||||||
|
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:#ff9800;font-size:0.8em;'
|
||||||
|
}, type + ' (missing)');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSslBadge: function(status) {
|
||||||
|
var color, text;
|
||||||
|
if (status === 'missing') {
|
||||||
|
color = '#9e9e9e';
|
||||||
|
text = 'No SSL';
|
||||||
|
} else if (status === 'expired') {
|
||||||
|
color = '#f44336';
|
||||||
|
text = 'Expired';
|
||||||
|
} else if (status && status.indexOf('expiring:') === 0) {
|
||||||
|
var days = status.split(':')[1];
|
||||||
|
color = '#ff9800';
|
||||||
|
text = 'Expires in ' + days + 'd';
|
||||||
|
} else if (status && status.indexOf('valid:') === 0) {
|
||||||
|
var days = status.split(':')[1];
|
||||||
|
color = '#4CAF50';
|
||||||
|
text = 'Valid (' + days + 'd)';
|
||||||
|
} else {
|
||||||
|
color = '#4CAF50';
|
||||||
|
text = 'Valid';
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('span', {
|
||||||
|
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:' + color + ';font-size:0.8em;'
|
||||||
|
}, text);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderWafBadge: function(bypass) {
|
||||||
|
if (bypass) {
|
||||||
|
return E('span', {
|
||||||
|
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:#f44336;font-size:0.8em;'
|
||||||
|
}, 'WAF Bypass');
|
||||||
|
} else {
|
||||||
|
return E('span', {
|
||||||
|
'style': 'display:inline-block;padding:2px 8px;margin:2px;border-radius:3px;color:#fff;background:#2196F3;font-size:0.8em;'
|
||||||
|
}, 'WAF Protected');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSync: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
ui.showModal(_('Syncing Routes...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Please wait...'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
callSyncRoutes().then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res && res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Routes synchronized successfully')), 'success');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Error: ') + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAddRoute: function(domain, port) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (!port) {
|
||||||
|
// Ask for port
|
||||||
|
ui.showModal(_('Add Route'), [
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('p', {}, _('Add mitmproxy route for: %s').format(domain)),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Backend Port')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', { 'type': 'number', 'id': 'route-port', 'value': '443', 'style': 'width:100px;' })
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'right' }, [
|
||||||
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button-positive',
|
||||||
|
'click': function() {
|
||||||
|
var port = parseInt(document.getElementById('route-port').value, 10);
|
||||||
|
if (port > 0) {
|
||||||
|
ui.hideModal();
|
||||||
|
self.doAddRoute(domain, port);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'style': 'margin-left:10px;'
|
||||||
|
}, _('Add Route'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
this.doAddRoute(domain, parseInt(port, 10));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
doAddRoute: function(domain, port) {
|
||||||
|
ui.showModal(_('Adding Route...'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Please wait...'))
|
||||||
|
]);
|
||||||
|
|
||||||
|
callAddRoute(domain, port).then(function(res) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (res && res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Route added successfully')), 'success');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Error: ') + (res.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderVhostRow: function(vhost) {
|
||||||
|
var self = this;
|
||||||
|
var missingRoutes = !vhost.has_route_out || !vhost.has_route_in;
|
||||||
|
|
||||||
|
return E('tr', { 'class': vhost.active ? '' : 'inactive' }, [
|
||||||
|
E('td', {}, [
|
||||||
|
E('a', {
|
||||||
|
'href': 'https://' + vhost.domain,
|
||||||
|
'target': '_blank',
|
||||||
|
'style': 'color:#1976D2;text-decoration:none;'
|
||||||
|
}, vhost.domain)
|
||||||
|
]),
|
||||||
|
E('td', {}, vhost.backend || '-'),
|
||||||
|
E('td', { 'style': 'text-align:center;' }, vhost.backend_port || '-'),
|
||||||
|
E('td', {}, [
|
||||||
|
this.renderRouteBadge(vhost.has_route_out, 'OUT'),
|
||||||
|
this.renderRouteBadge(vhost.has_route_in, 'IN')
|
||||||
|
]),
|
||||||
|
E('td', {}, this.renderSslBadge(vhost.ssl_status)),
|
||||||
|
E('td', {}, this.renderWafBadge(vhost.waf_bypass)),
|
||||||
|
E('td', {}, [
|
||||||
|
vhost.active ?
|
||||||
|
E('span', { 'style': 'color:#4CAF50;font-weight:bold;' }, 'Active') :
|
||||||
|
E('span', { 'style': 'color:#9e9e9e;' }, 'Inactive'),
|
||||||
|
missingRoutes ? E('button', {
|
||||||
|
'class': 'btn cbi-button',
|
||||||
|
'click': function() { self.handleAddRoute(vhost.domain, vhost.backend_port); },
|
||||||
|
'style': 'margin-left:10px;font-size:0.8em;padding:2px 8px;'
|
||||||
|
}, _('Add Route')) : null
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var self = this;
|
||||||
|
var vhosts = data.vhosts || [];
|
||||||
|
|
||||||
|
// Sort by domain
|
||||||
|
vhosts.sort(function(a, b) {
|
||||||
|
return a.domain.localeCompare(b.domain);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count stats
|
||||||
|
var totalVhosts = vhosts.length;
|
||||||
|
var activeVhosts = vhosts.filter(function(v) { return v.active; }).length;
|
||||||
|
var missingRoutes = vhosts.filter(function(v) { return !v.has_route_out || !v.has_route_in; }).length;
|
||||||
|
var wafBypassed = vhosts.filter(function(v) { return v.waf_bypass; }).length;
|
||||||
|
|
||||||
|
var content = [];
|
||||||
|
|
||||||
|
// Header
|
||||||
|
content.push(E('h2', {}, _('Routes Status Dashboard')));
|
||||||
|
|
||||||
|
// Service Status
|
||||||
|
content.push(E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Service Status')),
|
||||||
|
E('div', { 'style': 'margin:10px 0;' }, [
|
||||||
|
this.renderStatusBadge(data.haproxy_running, 'HAProxy'),
|
||||||
|
this.renderStatusBadge(data.mitmproxy_running, 'mitmproxy')
|
||||||
|
]),
|
||||||
|
E('p', { 'style': 'color:#666;' }, _('Host IP: %s').format(data.host_ip || '192.168.255.1'))
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
content.push(E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Statistics')),
|
||||||
|
E('div', { 'style': 'display:flex;gap:30px;margin:15px 0;' }, [
|
||||||
|
E('div', { 'style': 'text-align:center;' }, [
|
||||||
|
E('div', { 'style': 'font-size:2em;font-weight:bold;color:#1976D2;' }, String(totalVhosts)),
|
||||||
|
E('div', { 'style': 'color:#666;' }, _('Total Vhosts'))
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'text-align:center;' }, [
|
||||||
|
E('div', { 'style': 'font-size:2em;font-weight:bold;color:#4CAF50;' }, String(activeVhosts)),
|
||||||
|
E('div', { 'style': 'color:#666;' }, _('Active'))
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'text-align:center;' }, [
|
||||||
|
E('div', { 'style': 'font-size:2em;font-weight:bold;color:' + (missingRoutes > 0 ? '#ff9800' : '#4CAF50') + ';' }, String(missingRoutes)),
|
||||||
|
E('div', { 'style': 'color:#666;' }, _('Missing Routes'))
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'text-align:center;' }, [
|
||||||
|
E('div', { 'style': 'font-size:2em;font-weight:bold;color:' + (wafBypassed > 0 ? '#f44336' : '#4CAF50') + ';' }, String(wafBypassed)),
|
||||||
|
E('div', { 'style': 'color:#666;' }, _('WAF Bypassed'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Vhosts Table
|
||||||
|
var vhostRows = vhosts.map(function(v) { return self.renderVhostRow(v); });
|
||||||
|
|
||||||
|
content.push(E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Virtual Hosts')),
|
||||||
|
E('div', { 'class': 'cbi-page-actions', 'style': 'margin-bottom:15px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'btn cbi-button-action',
|
||||||
|
'click': function() { self.handleSync(); }
|
||||||
|
}, _('Sync Routes from HAProxy'))
|
||||||
|
]),
|
||||||
|
vhosts.length > 0 ?
|
||||||
|
E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, _('Domain')),
|
||||||
|
E('th', { 'class': 'th' }, _('Backend')),
|
||||||
|
E('th', { 'class': 'th', 'style': 'text-align:center;' }, _('Port')),
|
||||||
|
E('th', { 'class': 'th' }, _('Routes')),
|
||||||
|
E('th', { 'class': 'th' }, _('SSL')),
|
||||||
|
E('th', { 'class': 'th' }, _('WAF')),
|
||||||
|
E('th', { 'class': 'th' }, _('Status'))
|
||||||
|
])
|
||||||
|
].concat(vhostRows)) :
|
||||||
|
E('p', { 'style': 'color:#666;' }, _('No virtual hosts configured.'))
|
||||||
|
]));
|
||||||
|
|
||||||
|
return E('div', { 'class': 'cbi-map' }, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
205
package/secubox/luci-app-routes-status/root/usr/libexec/rpcd/luci.routes-status
Executable file
205
package/secubox/luci-app-routes-status/root/usr/libexec/rpcd/luci.routes-status
Executable file
@ -0,0 +1,205 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# RPCD handler for Routes Status dashboard
|
||||||
|
# Shows HAProxy vhosts and mitmproxy route configuration status
|
||||||
|
|
||||||
|
. /usr/share/libubox/jshn.sh
|
||||||
|
|
||||||
|
MITMPROXY_ROUTES="/srv/mitmproxy/haproxy-routes.json"
|
||||||
|
MITMPROXY_IN_ROUTES="/srv/mitmproxy-in/haproxy-routes.json"
|
||||||
|
HAPROXY_CERTS="/srv/haproxy/certs"
|
||||||
|
|
||||||
|
# Get host LAN IP for route configuration
|
||||||
|
get_host_ip() {
|
||||||
|
uci -q get network.lan.ipaddr || echo "192.168.255.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main status method - optimized to fetch data once
|
||||||
|
method_status() {
|
||||||
|
local host_ip=$(get_host_ip)
|
||||||
|
local haproxy_running=$(pgrep haproxy >/dev/null 2>&1 && echo "true" || echo "false")
|
||||||
|
local mitmproxy_running=$(pgrep -f mitmproxy >/dev/null 2>&1 && echo "true" || echo "false")
|
||||||
|
|
||||||
|
# Fetch vhost list once to temp file
|
||||||
|
local vhost_tmp="/tmp/vhosts_$$"
|
||||||
|
if command -v haproxyctl >/dev/null 2>&1; then
|
||||||
|
haproxyctl vhost list 2>/dev/null | tail -n +3 > "$vhost_tmp"
|
||||||
|
else
|
||||||
|
touch "$vhost_tmp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean haproxy_running "$haproxy_running"
|
||||||
|
json_add_boolean mitmproxy_running "$mitmproxy_running"
|
||||||
|
json_add_string host_ip "$host_ip"
|
||||||
|
|
||||||
|
json_add_array vhosts
|
||||||
|
|
||||||
|
# Process vhost data line by line (using file redirection to avoid subshell)
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
|
||||||
|
# Parse line: " domain.com -> backend_name [enabled] SSL ..."
|
||||||
|
local domain=$(echo "$line" | awk '{print $1}')
|
||||||
|
local backend=$(echo "$line" | awk '{print $3}')
|
||||||
|
local enabled=$(echo "$line" | grep -qF '[enabled]' && echo "true" || echo "false")
|
||||||
|
|
||||||
|
[ -z "$domain" ] && continue
|
||||||
|
|
||||||
|
# Check mitmproxy routes (grep for domain in JSON)
|
||||||
|
local has_route_out="false"
|
||||||
|
local has_route_in="false"
|
||||||
|
if [ -f "$MITMPROXY_ROUTES" ] && grep -qF "\"$domain\"" "$MITMPROXY_ROUTES" 2>/dev/null; then
|
||||||
|
has_route_out="true"
|
||||||
|
fi
|
||||||
|
if [ -f "$MITMPROXY_IN_ROUTES" ] && grep -qF "\"$domain\"" "$MITMPROXY_IN_ROUTES" 2>/dev/null; then
|
||||||
|
has_route_in="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check SSL cert
|
||||||
|
local ssl_status="missing"
|
||||||
|
[ -f "$HAPROXY_CERTS/${domain}.pem" ] && ssl_status="valid"
|
||||||
|
|
||||||
|
# WAF bypass check
|
||||||
|
local waf_bypass="false"
|
||||||
|
[ "$backend" != "mitmproxy_inspector" ] && waf_bypass="true"
|
||||||
|
|
||||||
|
json_add_object ""
|
||||||
|
json_add_string domain "$domain"
|
||||||
|
json_add_string backend "$backend"
|
||||||
|
json_add_string backend_port ""
|
||||||
|
json_add_boolean active "$enabled"
|
||||||
|
json_add_string ssl_status "$ssl_status"
|
||||||
|
json_add_boolean has_route_out "$has_route_out"
|
||||||
|
json_add_boolean has_route_in "$has_route_in"
|
||||||
|
json_add_string route_target_out ""
|
||||||
|
json_add_string route_target_in ""
|
||||||
|
json_add_boolean waf_bypass "$waf_bypass"
|
||||||
|
json_close_object
|
||||||
|
done < "$vhost_tmp"
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_dump
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -f "$vhost_tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sync routes from HAProxy backends to mitmproxy
|
||||||
|
method_sync_routes() {
|
||||||
|
local result
|
||||||
|
if [ -x /usr/sbin/mitmproxyctl ]; then
|
||||||
|
result=$(/usr/sbin/mitmproxyctl sync-routes 2>&1)
|
||||||
|
json_init
|
||||||
|
json_add_boolean success true
|
||||||
|
json_add_string output "$result"
|
||||||
|
json_dump
|
||||||
|
else
|
||||||
|
json_init
|
||||||
|
json_add_boolean success false
|
||||||
|
json_add_string error "mitmproxyctl not found"
|
||||||
|
json_dump
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add a missing route for a domain
|
||||||
|
method_add_route() {
|
||||||
|
local domain port
|
||||||
|
|
||||||
|
# Read JSON input
|
||||||
|
read -r input
|
||||||
|
json_load "$input"
|
||||||
|
json_get_var domain domain
|
||||||
|
json_get_var port port
|
||||||
|
|
||||||
|
if [ -z "$domain" ] || [ -z "$port" ]; then
|
||||||
|
json_init
|
||||||
|
json_add_boolean success false
|
||||||
|
json_add_string error "Missing domain or port parameter"
|
||||||
|
json_dump
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local host_ip=$(get_host_ip)
|
||||||
|
|
||||||
|
# Add route to both mitmproxy route files
|
||||||
|
local success="true"
|
||||||
|
local errors=""
|
||||||
|
|
||||||
|
for routes_file in "$MITMPROXY_ROUTES" "$MITMPROXY_IN_ROUTES"; do
|
||||||
|
if [ -f "$routes_file" ]; then
|
||||||
|
# Create temp file with new route
|
||||||
|
local tmpfile=$(mktemp)
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
jq --arg d "$domain" --arg h "$host_ip" --argjson p "$port" \
|
||||||
|
'. + {($d): [$h, $p]}' "$routes_file" > "$tmpfile" 2>/dev/null
|
||||||
|
else
|
||||||
|
# Fallback: manual JSON manipulation
|
||||||
|
# Remove trailing } and add new entry
|
||||||
|
sed 's/}$//' "$routes_file" > "$tmpfile"
|
||||||
|
# Check if file has content (not empty object)
|
||||||
|
if grep -q '": \[' "$routes_file"; then
|
||||||
|
printf ',\n "%s": ["%s", %s]\n}\n' "$domain" "$host_ip" "$port" >> "$tmpfile"
|
||||||
|
else
|
||||||
|
printf ' "%s": ["%s", %s]\n}\n' "$domain" "$host_ip" "$port" >> "$tmpfile"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -s "$tmpfile" ]; then
|
||||||
|
mv "$tmpfile" "$routes_file"
|
||||||
|
else
|
||||||
|
rm -f "$tmpfile"
|
||||||
|
success="false"
|
||||||
|
errors="$errors Failed to update $routes_file."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Restart mitmproxy to apply changes
|
||||||
|
if [ "$success" = "true" ]; then
|
||||||
|
/etc/init.d/mitmproxy restart >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init
|
||||||
|
json_add_boolean success "$success"
|
||||||
|
[ -n "$errors" ] && json_add_string error "$errors"
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
# List available methods
|
||||||
|
list_methods() {
|
||||||
|
json_init
|
||||||
|
json_add_object status
|
||||||
|
json_close_object
|
||||||
|
json_add_object sync_routes
|
||||||
|
json_close_object
|
||||||
|
json_add_object add_route
|
||||||
|
json_add_string domain "string"
|
||||||
|
json_add_int port 0
|
||||||
|
json_close_object
|
||||||
|
json_dump
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
list)
|
||||||
|
list_methods
|
||||||
|
;;
|
||||||
|
call)
|
||||||
|
case "$2" in
|
||||||
|
status)
|
||||||
|
method_status
|
||||||
|
;;
|
||||||
|
sync_routes)
|
||||||
|
method_sync_routes
|
||||||
|
;;
|
||||||
|
add_route)
|
||||||
|
method_add_route
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown method"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo '{"error":"Unknown action"}'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"admin/status/routes": {
|
||||||
|
"title": "Routes Status",
|
||||||
|
"order": 15,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "routes-status/overview"
|
||||||
|
},
|
||||||
|
"depends": {
|
||||||
|
"acl": ["luci-app-routes-status"],
|
||||||
|
"uci": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"luci-app-routes-status": {
|
||||||
|
"description": "Grant access to Routes Status dashboard",
|
||||||
|
"read": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.routes-status": ["status"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"ubus": {
|
||||||
|
"luci.routes-status": ["sync_routes", "add_route"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user