diff --git a/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/service-registry/api.js b/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/service-registry/api.js
index 4e13a396..2735d499 100644
--- a/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/service-registry/api.js
+++ b/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/service-registry/api.js
@@ -87,6 +87,19 @@ var callSaveLandingConfig = rpc.declare({
expect: {}
});
+var callCheckServiceHealth = rpc.declare({
+ object: 'luci.service-registry',
+ method: 'check_service_health',
+ params: ['service_id', 'domain'],
+ expect: {}
+});
+
+var callCheckAllHealth = rpc.declare({
+ object: 'luci.service-registry',
+ method: 'check_all_health',
+ expect: {}
+});
+
// HAProxy status for provider info
var callHAProxyStatus = rpc.declare({
object: 'luci.haproxy',
@@ -220,5 +233,37 @@ return baseclass.extend({
// Full publish with HAProxy + Tor
fullPublish: function(name, port, domain) {
return this.publishService(name, port, domain, true, 'services', '');
+ },
+
+ // Check health of a single service
+ checkServiceHealth: function(serviceId, domain) {
+ return callCheckServiceHealth(serviceId || '', domain || '');
+ },
+
+ // Check health of all published services
+ checkAllHealth: function() {
+ return callCheckAllHealth();
+ },
+
+ // Get dashboard data with health status
+ getDashboardDataWithHealth: function() {
+ return Promise.all([
+ callListServices().catch(function(e) { console.error('list_services failed:', e); return { services: [], providers: {} }; }),
+ callListCategories().catch(function(e) { console.error('list_categories failed:', e); return { categories: [] }; }),
+ callGetLandingConfig().catch(function(e) { console.error('get_landing_config failed:', e); return {}; }),
+ callHAProxyStatus().catch(function() { return { enabled: false }; }),
+ callTorStatus().catch(function() { return { enabled: false }; }),
+ callCheckAllHealth().catch(function(e) { console.error('check_all_health failed:', e); return { health: {} }; })
+ ]).then(function(results) {
+ return {
+ services: results[0].services || [],
+ providers: results[0].providers || {},
+ categories: results[1].categories || [],
+ landing: results[2],
+ haproxy: results[3],
+ tor: results[4],
+ health: results[5].health || {}
+ };
+ });
}
});
diff --git a/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/overview.js b/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/overview.js
index 62155e63..547ad6e2 100644
--- a/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/overview.js
+++ b/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/overview.js
@@ -12,6 +12,13 @@ var catIcons = {
'monitoring': '📊', 'other': '🔗'
};
+// Health status icons
+var healthIcons = {
+ 'dns': { 'ok': '🌐', 'failed': '❌', 'none': '⚪' },
+ 'cert': { 'ok': '🔒', 'warning': '⚠️', 'critical': '🔴', 'expired': '💀', 'missing': '⚪', 'none': '⚪' },
+ 'firewall': { 'ok': '✅', 'partial': '⚠️', 'closed': '🚫' }
+};
+
// Generate QR code using QR Server API (free, reliable)
function generateQRCodeImg(data, size) {
var url = 'https://api.qrserver.com/v1/create-qr-code/?size=' + size + 'x' + size + '&data=' + encodeURIComponent(data);
@@ -21,9 +28,10 @@ function generateQRCodeImg(data, size) {
return view.extend({
title: _('Service Registry'),
pollInterval: 30,
+ healthData: null,
load: function() {
- return api.getDashboardData();
+ return api.getDashboardDataWithHealth();
},
render: function(data) {
@@ -31,6 +39,9 @@ return view.extend({
var services = data.services || [];
var providers = data.providers || {};
+ // Store health data for service lookups
+ this.healthData = data.health || {};
+
// Load CSS
var style = document.createElement('style');
style.textContent = this.getStyles();
@@ -41,12 +52,177 @@ return view.extend({
return E('div', { 'class': 'sr-compact' }, [
this.renderHeader(services, providers, data.haproxy, data.tor),
+ this.renderHealthSummary(data.health),
+ this.renderUrlChecker(),
this.renderSection('📡 Published Services', published, true),
this.renderSection('🔍 Discovered Services', unpublished, false),
this.renderLandingLink(data.landing)
]);
},
+ renderHealthSummary: function(health) {
+ if (!health || !health.firewall) return E('div');
+
+ var firewallStatus = health.firewall.status || 'unknown';
+ var firewallIcon = healthIcons.firewall[firewallStatus] || '❓';
+ var haproxyStatus = health.haproxy && health.haproxy.status === 'running' ? '🟢' : '🔴';
+ var torStatus = health.tor && health.tor.status === 'running' ? '🟢' : '🔴';
+
+ // Count service health
+ var services = health.services || [];
+ var dnsOk = services.filter(function(s) { return s.dns_status === 'ok'; }).length;
+ var certOk = services.filter(function(s) { return s.cert_status === 'ok'; }).length;
+ var certWarn = services.filter(function(s) { return s.cert_status === 'warning' || s.cert_status === 'critical'; }).length;
+
+ return E('div', { 'class': 'sr-health-bar' }, [
+ E('span', { 'class': 'sr-health-item', 'title': 'Firewall ports 80/443' },
+ firewallIcon + ' Firewall: ' + firewallStatus),
+ E('span', { 'class': 'sr-health-item', 'title': 'HAProxy container' },
+ haproxyStatus + ' HAProxy'),
+ E('span', { 'class': 'sr-health-item', 'title': 'Tor daemon' },
+ torStatus + ' Tor'),
+ services.length > 0 ? E('span', { 'class': 'sr-health-item' },
+ '🌐 DNS: ' + dnsOk + '/' + services.length) : null,
+ services.length > 0 ? E('span', { 'class': 'sr-health-item' },
+ '🔒 Certs: ' + certOk + '/' + services.length +
+ (certWarn > 0 ? ' (⚠️ ' + certWarn + ')' : '')) : null
+ ].filter(Boolean));
+ },
+
+ renderUrlChecker: function() {
+ var self = this;
+ return E('div', { 'class': 'sr-wizard-card' }, [
+ E('div', { 'class': 'sr-wizard-header' }, [
+ E('span', { 'class': 'sr-wizard-icon' }, '🔍'),
+ E('span', { 'class': 'sr-wizard-title' }, 'URL Readiness Checker'),
+ E('span', { 'class': 'sr-wizard-desc' }, 'Check if a domain is ready to be hosted')
+ ]),
+ E('div', { 'class': 'sr-wizard-form' }, [
+ E('input', {
+ 'type': 'text',
+ 'id': 'url-check-domain',
+ 'placeholder': 'Enter domain (e.g., example.com)',
+ 'class': 'sr-wizard-input'
+ }),
+ E('button', {
+ 'class': 'cbi-button cbi-button-action',
+ 'click': ui.createHandlerFn(this, 'handleUrlCheck')
+ }, '🔍 Check')
+ ]),
+ E('div', { 'id': 'url-check-results', 'class': 'sr-check-results' })
+ ]);
+ },
+
+ handleUrlCheck: function() {
+ var self = this;
+ var domain = document.getElementById('url-check-domain').value.trim();
+ var resultsDiv = document.getElementById('url-check-results');
+
+ if (!domain) {
+ resultsDiv.innerHTML = '
Please enter a domain
';
+ return;
+ }
+
+ // Clean domain (remove protocol if present)
+ domain = domain.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
+
+ resultsDiv.innerHTML = '🔄 Checking ' + domain + '...
';
+
+ api.checkServiceHealth('', domain).then(function(result) {
+ if (!result.success) {
+ resultsDiv.innerHTML = '❌ Check failed: ' + (result.error || 'Unknown error') + '
';
+ return;
+ }
+
+ var html = '';
+
+ // DNS Status
+ var dnsStatus = result.dns || {};
+ var dnsIcon = healthIcons.dns[dnsStatus.status] || '❓';
+ var dnsClass = dnsStatus.status === 'ok' ? 'sr-check-ok' : 'sr-check-fail';
+ html += '
';
+ html += '' + dnsIcon + '';
+ html += 'DNS Resolution';
+ if (dnsStatus.status === 'ok') {
+ html += '✅ Resolves to ' + dnsStatus.resolved_ip + '';
+ } else {
+ html += '❌ DNS not configured or not resolving';
+ }
+ html += '
';
+
+ // Firewall Status
+ var fwStatus = result.firewall || {};
+ var fwIcon = healthIcons.firewall[fwStatus.status] || '❓';
+ var fwClass = fwStatus.status === 'ok' ? 'sr-check-ok' : (fwStatus.status === 'partial' ? 'sr-check-warn' : 'sr-check-fail');
+ html += '
';
+ html += '' + fwIcon + '';
+ html += 'Firewall Ports';
+ var ports = [];
+ if (fwStatus.http_open) ports.push('80');
+ if (fwStatus.https_open) ports.push('443');
+ html += '' + (ports.length ? 'Open: ' + ports.join(', ') : '❌ Ports 80/443 not open') + '';
+ html += '
';
+
+ // Certificate Status
+ var certStatus = result.certificate || {};
+ var certIcon = healthIcons.cert[certStatus.status] || '❓';
+ var certClass = certStatus.status === 'ok' ? 'sr-check-ok' : (certStatus.status === 'warning' ? 'sr-check-warn' : 'sr-check-fail');
+ html += '
';
+ html += '' + certIcon + '';
+ html += 'SSL Certificate';
+ if (certStatus.status === 'ok' || certStatus.status === 'warning') {
+ html += '' + certStatus.days_left + ' days remaining';
+ } else if (certStatus.status === 'expired') {
+ html += '❌ Certificate expired';
+ } else if (certStatus.status === 'missing') {
+ html += '⚪ No certificate (request via HAProxy)';
+ } else {
+ html += '⚪ Not applicable';
+ }
+ html += '
';
+
+ // HAProxy Status
+ var haStatus = result.haproxy || {};
+ var haIcon = haStatus.status === 'running' ? '🟢' : '🔴';
+ var haClass = haStatus.status === 'running' ? 'sr-check-ok' : 'sr-check-fail';
+ html += '
';
+ html += '' + haIcon + '';
+ html += 'HAProxy';
+ html += '' + (haStatus.status === 'running' ? '✅ Running' : '❌ Not running') + '';
+ html += '
';
+
+ html += '
';
+
+ // Summary and recommendation
+ var allOk = dnsStatus.status === 'ok' && fwStatus.status === 'ok' && haStatus.status === 'running';
+ var needsCert = certStatus.status === 'missing';
+
+ html += '';
+ if (allOk && !needsCert) {
+ html += '
✅ ' + domain + ' is ready and serving!
';
+ } else if (allOk && needsCert) {
+ html += '
⚠️ ' + domain + ' is ready - just need SSL certificate
';
+ html += '
📜 Request Certificate';
+ } else {
+ html += '
❌ ' + domain + ' needs configuration
';
+ if (dnsStatus.status !== 'ok') {
+ html += '
💡 Point DNS A record to your public IP
';
+ }
+ if (fwStatus.status !== 'ok') {
+ html += '
💡 Open ports 80 and 443 in firewall
';
+ }
+ if (haStatus.status !== 'running') {
+ html += '
💡 Start HAProxy container
';
+ }
+ }
+ html += '
';
+
+ resultsDiv.innerHTML = html;
+ }).catch(function(err) {
+ resultsDiv.innerHTML = '❌ Error: ' + err.message + '
';
+ });
+ },
+
renderHeader: function(services, providers, haproxy, tor) {
var published = services.filter(function(s) { return s.published; }).length;
var running = services.filter(function(s) { return s.status === 'running'; }).length;
@@ -135,20 +311,46 @@ return view.extend({
portDisplay = ':' + service.haproxy.backend_port;
}
- // SSL/Cert badge
- var sslBadge = null;
- if (service.haproxy) {
+ // Get health status for this domain (if available)
+ var healthBadges = [];
+ var domain = service.haproxy && service.haproxy.domain;
+ if (domain && this.healthData && this.healthData.services) {
+ var svcHealth = this.healthData.services.find(function(h) {
+ return h.domain === domain;
+ });
+ if (svcHealth) {
+ // DNS badge
+ var dnsIcon = healthIcons.dns[svcHealth.dns_status] || '❓';
+ var dnsTitle = svcHealth.dns_status === 'ok' ?
+ 'DNS OK: ' + svcHealth.dns_ip : 'DNS: ' + svcHealth.dns_status;
+ healthBadges.push(E('span', {
+ 'class': 'sr-badge sr-badge-dns sr-badge-' + svcHealth.dns_status,
+ 'title': dnsTitle
+ }, dnsIcon));
+
+ // Cert badge
+ var certIcon = healthIcons.cert[svcHealth.cert_status] || '❓';
+ var certTitle = svcHealth.cert_status === 'ok' || svcHealth.cert_status === 'warning' ?
+ 'Cert: ' + svcHealth.cert_days + ' days' : 'Cert: ' + svcHealth.cert_status;
+ healthBadges.push(E('span', {
+ 'class': 'sr-badge sr-badge-cert sr-badge-' + svcHealth.cert_status,
+ 'title': certTitle
+ }, certIcon));
+ }
+ }
+
+ // SSL/Cert badge (fallback if no health data)
+ if (healthBadges.length === 0 && service.haproxy) {
if (service.haproxy.acme) {
- sslBadge = E('span', { 'class': 'sr-badge sr-badge-acme', 'title': 'ACME Certificate' }, '🔒');
+ healthBadges.push(E('span', { 'class': 'sr-badge sr-badge-acme', 'title': 'ACME Certificate' }, '🔒'));
} else if (service.haproxy.ssl) {
- sslBadge = E('span', { 'class': 'sr-badge sr-badge-ssl', 'title': 'SSL Enabled' }, '🔐');
+ healthBadges.push(E('span', { 'class': 'sr-badge sr-badge-ssl', 'title': 'SSL Enabled' }, '🔐'));
}
}
// Tor badge
- var torBadge = null;
if (service.tor && service.tor.enabled) {
- torBadge = E('span', { 'class': 'sr-badge sr-badge-tor', 'title': 'Tor Hidden Service' }, '🧅');
+ healthBadges.push(E('span', { 'class': 'sr-badge sr-badge-tor', 'title': 'Tor Hidden Service' }, '🧅'));
}
// QR button for published services with URLs
@@ -161,6 +363,16 @@ return view.extend({
}, '📱');
}
+ // Health check button for published services with domains
+ var checkBtn = null;
+ if (isPublished && domain) {
+ checkBtn = E('button', {
+ 'class': 'sr-btn sr-btn-check',
+ 'title': 'Check Health',
+ 'click': ui.createHandlerFn(this, 'handleServiceHealthCheck', service)
+ }, '🔍');
+ }
+
// Action button
var actionBtn;
if (isPublished) {
@@ -187,12 +399,24 @@ return view.extend({
E('span', { 'class': 'sr-col-url' },
urlDisplay ? E('a', { 'href': urlDisplay.startsWith('http') ? urlDisplay : 'http://' + urlDisplay, 'target': '_blank' }, urlDisplay) : '-'
),
- E('span', { 'class': 'sr-col-badges' }, [sslBadge, torBadge].filter(Boolean)),
- E('span', { 'class': 'sr-col-qr' }, qrBtn),
+ E('span', { 'class': 'sr-col-badges' }, healthBadges),
+ E('span', { 'class': 'sr-col-qr' }, [qrBtn, checkBtn].filter(Boolean)),
E('span', { 'class': 'sr-col-action' }, actionBtn)
]);
},
+ handleServiceHealthCheck: function(service) {
+ var self = this;
+ var domain = service.haproxy && service.haproxy.domain;
+ if (!domain) return;
+
+ document.getElementById('url-check-domain').value = domain;
+ this.handleUrlCheck();
+
+ // Scroll to the checker
+ document.querySelector('.sr-wizard-card').scrollIntoView({ behavior: 'smooth' });
+ },
+
handleShowQR: function(service) {
var urls = service.urls || {};
var qrBoxes = [];
@@ -321,6 +545,41 @@ return view.extend({
.sr-providers-bar { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.sr-provider-badge { background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 12px; font-size: 0.8em; }
+ /* Health Summary Bar */
+ .sr-health-bar { display: flex; gap: 15px; margin-bottom: 15px; padding: 10px 15px; background: #f0f7ff; border-radius: 6px; border-left: 4px solid #0099cc; flex-wrap: wrap; }
+ @media (prefers-color-scheme: dark) { .sr-health-bar { background: #1a2a3e; } }
+ .sr-health-item { font-size: 0.9em; }
+
+ /* URL Checker Wizard Card */
+ .sr-wizard-card { background: linear-gradient(135deg, #0a192f 0%, #172a45 100%); border-radius: 12px; padding: 20px; margin-bottom: 25px; color: #fff; }
+ .sr-wizard-header { display: flex; align-items: center; gap: 12px; margin-bottom: 15px; }
+ .sr-wizard-icon { font-size: 1.8em; }
+ .sr-wizard-title { font-size: 1.2em; font-weight: 600; }
+ .sr-wizard-desc { font-size: 0.85em; opacity: 0.7; margin-left: auto; }
+ .sr-wizard-form { display: flex; gap: 10px; align-items: center; }
+ .sr-wizard-input { flex: 1; padding: 10px 15px; border: 1px solid #334155; border-radius: 6px; background: #0f172a; color: #fff; font-size: 1em; }
+ .sr-wizard-input::placeholder { color: #64748b; }
+
+ /* Health Check Results */
+ .sr-check-results { margin-top: 15px; }
+ .sr-check-loading { text-align: center; padding: 20px; font-size: 1.1em; }
+ .sr-check-error { background: #450a0a; padding: 12px 15px; border-radius: 6px; color: #fca5a5; }
+ .sr-check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
+ .sr-check-item { background: #0f172a; padding: 12px 15px; border-radius: 8px; display: flex; align-items: center; gap: 10px; border-left: 3px solid #334155; }
+ .sr-check-item.sr-check-ok { border-left-color: #22c55e; }
+ .sr-check-item.sr-check-warn { border-left-color: #eab308; }
+ .sr-check-item.sr-check-fail { border-left-color: #ef4444; }
+ .sr-check-icon { font-size: 1.3em; }
+ .sr-check-label { font-weight: 600; font-size: 0.9em; min-width: 100px; }
+ .sr-check-value { font-size: 0.85em; opacity: 0.8; }
+ .sr-check-summary { margin-top: 15px; padding: 15px; background: #0f172a; border-radius: 8px; text-align: center; }
+ .sr-check-ready { font-size: 1.1em; color: #22c55e; font-weight: 600; }
+ .sr-check-almost { font-size: 1.1em; color: #eab308; font-weight: 600; }
+ .sr-check-notready { font-size: 1.1em; color: #ef4444; font-weight: 600; margin-bottom: 10px; }
+ .sr-check-tip { font-size: 0.85em; opacity: 0.8; margin-top: 5px; }
+ .sr-check-action { display: inline-block; margin-top: 10px; padding: 8px 16px; background: #0099cc; color: #fff; text-decoration: none; border-radius: 6px; font-size: 0.9em; }
+ .sr-check-action:hover { background: #00b3e6; }
+
.sr-section { margin-bottom: 25px; }
.sr-section-title { font-size: 1.1em; margin: 0 0 10px 0; padding-bottom: 8px; border-bottom: 2px solid #0ff; color: #0ff; }
.sr-empty-msg { color: #888; font-style: italic; padding: 15px; }
@@ -345,11 +604,17 @@ return view.extend({
.sr-col-url { flex: 2; min-width: 150px; font-size: 0.85em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sr-col-url a { color: #0099cc; text-decoration: none; }
.sr-col-url a:hover { text-decoration: underline; }
- .sr-col-badges { width: 50px; display: flex; gap: 4px; }
- .sr-col-qr { width: 36px; }
+ .sr-col-badges { width: 80px; display: flex; gap: 4px; }
+ .sr-col-qr { width: 60px; display: flex; gap: 4px; }
.sr-col-action { width: 36px; }
- .sr-badge { font-size: 0.85em; }
+ .sr-badge { font-size: 0.85em; cursor: help; }
+ .sr-badge-ok { opacity: 1; }
+ .sr-badge-warning { animation: pulse 2s infinite; }
+ .sr-badge-critical, .sr-badge-expired { animation: pulse 1s infinite; }
+ .sr-badge-missing, .sr-badge-none { opacity: 0.5; }
+ .sr-badge-failed { opacity: 1; }
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.sr-btn { border: none; background: transparent; cursor: pointer; font-size: 1em; padding: 4px 8px; border-radius: 4px; transition: all 0.15s; }
.sr-btn:hover { background: rgba(0,0,0,0.1); }
@@ -359,6 +624,8 @@ return view.extend({
.sr-btn-unpublish:hover { background: rgba(239,68,68,0.15); }
.sr-btn-qr { color: #0099cc; }
.sr-btn-qr:hover { background: rgba(0,153,204,0.15); }
+ .sr-btn-check { color: #8b5cf6; font-size: 0.9em; }
+ .sr-btn-check:hover { background: rgba(139,92,246,0.15); }
.sr-btn-regen { margin-left: 10px; font-size: 0.85em; }
.sr-qr-modal { display: flex; gap: 30px; justify-content: center; flex-wrap: wrap; padding: 20px 0; }
@@ -380,6 +647,8 @@ return view.extend({
@media (max-width: 768px) {
.sr-row { flex-wrap: wrap; }
.sr-col-url { flex-basis: 100%; order: 10; margin-top: 5px; }
+ .sr-wizard-form { flex-direction: column; }
+ .sr-wizard-input { width: 100%; }
}
`;
}
diff --git a/package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry b/package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry
index 65b62876..979e4714 100644
--- a/package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry
+++ b/package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry
@@ -880,6 +880,305 @@ _add_category() {
json_close_object
}
+# Helper: Check DNS resolution for a domain
+check_dns_resolution() {
+ local domain="$1"
+ local expected_ip="$2"
+
+ # Try nslookup first (most common on OpenWrt)
+ local resolved_ip
+ if command -v nslookup >/dev/null 2>&1; then
+ resolved_ip=$(nslookup "$domain" 2>/dev/null | grep -A1 "Name:" | grep "Address" | head -1 | awk '{print $2}')
+ [ -z "$resolved_ip" ] && resolved_ip=$(nslookup "$domain" 2>/dev/null | grep "Address" | tail -1 | awk '{print $2}' | grep -v "^$")
+ elif command -v host >/dev/null 2>&1; then
+ resolved_ip=$(host "$domain" 2>/dev/null | grep "has address" | head -1 | awk '{print $4}')
+ fi
+
+ if [ -n "$resolved_ip" ]; then
+ echo "$resolved_ip"
+ return 0
+ fi
+ return 1
+}
+
+# Helper: Get WAN IP address
+get_wan_ip() {
+ local wan_ip=""
+ # Try to get WAN IP from interface
+ wan_ip=$(uci -q get network.wan.ipaddr)
+ if [ -z "$wan_ip" ]; then
+ # Try to get from ip command
+ wan_ip=$(ip -4 addr show dev eth0 2>/dev/null | grep -oE 'inet [0-9.]+' | head -1 | awk '{print $2}')
+ fi
+ if [ -z "$wan_ip" ]; then
+ # Try to get public IP (uses local IP for comparison)
+ wan_ip=$(ifconfig 2>/dev/null | grep -E "inet addr:[0-9]" | grep -v "127.0.0.1" | head -1 | sed 's/.*inet addr:\([0-9.]*\).*/\1/')
+ fi
+ echo "$wan_ip"
+}
+
+# Helper: Check certificate expiry
+check_cert_expiry() {
+ local domain="$1"
+ local cert_file="/srv/haproxy/certs/${domain}.pem"
+
+ if [ ! -f "$cert_file" ]; then
+ # Try the ACME path
+ cert_file="/etc/acme/${domain}_ecc/${domain}.cer"
+ [ ! -f "$cert_file" ] && cert_file="/etc/acme/${domain}/${domain}.cer"
+ fi
+
+ if [ -f "$cert_file" ]; then
+ # Get expiry date using openssl
+ local expiry_date
+ expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2)
+ if [ -n "$expiry_date" ]; then
+ # Convert to epoch
+ local expiry_epoch
+ expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null)
+ local now_epoch
+ now_epoch=$(date +%s)
+ local days_left
+ if [ -n "$expiry_epoch" ]; then
+ days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
+ echo "$days_left"
+ return 0
+ fi
+ fi
+ fi
+ return 1
+}
+
+# Helper: Check if external port is accessible (basic check via firewall rules)
+check_port_firewall_open() {
+ local port="$1"
+ local rule_name=""
+
+ case "$port" in
+ 80) rule_name="HAProxy-HTTP" ;;
+ 443) rule_name="HAProxy-HTTPS" ;;
+ esac
+
+ # Check if firewall rule exists and is enabled
+ local i=0
+ while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do
+ local name=$(uci -q get firewall.@rule[$i].name)
+ local enabled=$(uci -q get firewall.@rule[$i].enabled)
+ local dest_port=$(uci -q get firewall.@rule[$i].dest_port)
+ if [ "$name" = "$rule_name" ] || [ "$dest_port" = "$port" ]; then
+ if [ "$enabled" != "0" ]; then
+ return 0
+ fi
+ fi
+ i=$((i + 1))
+ done
+ return 1
+}
+
+# Check health status for a service
+method_check_service_health() {
+ local service_id domain
+
+ read -r input
+ json_load "$input"
+ json_get_var service_id service_id
+ json_get_var domain domain ""
+
+ json_init
+
+ if [ -z "$service_id" ] && [ -z "$domain" ]; then
+ json_add_boolean "success" 0
+ json_add_string "error" "service_id or domain is required"
+ json_dump
+ return
+ fi
+
+ # If only service_id provided, get domain from config
+ if [ -z "$domain" ] && [ -n "$service_id" ]; then
+ config_load "$UCI_CONFIG"
+ config_get domain "$service_id" haproxy_domain ""
+ fi
+
+ json_add_boolean "success" 1
+ json_add_string "service_id" "$service_id"
+ json_add_string "domain" "$domain"
+
+ # DNS check
+ json_add_object "dns"
+ if [ -n "$domain" ]; then
+ local resolved_ip
+ resolved_ip=$(check_dns_resolution "$domain")
+ if [ -n "$resolved_ip" ]; then
+ json_add_string "status" "ok"
+ json_add_string "resolved_ip" "$resolved_ip"
+ else
+ json_add_string "status" "failed"
+ json_add_string "error" "DNS resolution failed"
+ fi
+ else
+ json_add_string "status" "none"
+ fi
+ json_close_object
+
+ # Certificate check
+ json_add_object "certificate"
+ if [ -n "$domain" ]; then
+ local days_left
+ days_left=$(check_cert_expiry "$domain")
+ if [ -n "$days_left" ]; then
+ if [ "$days_left" -lt 0 ]; then
+ json_add_string "status" "expired"
+ elif [ "$days_left" -lt 7 ]; then
+ json_add_string "status" "critical"
+ elif [ "$days_left" -lt 30 ]; then
+ json_add_string "status" "warning"
+ else
+ json_add_string "status" "ok"
+ fi
+ json_add_int "days_left" "$days_left"
+ else
+ json_add_string "status" "missing"
+ fi
+ else
+ json_add_string "status" "none"
+ fi
+ json_close_object
+
+ # Port/Firewall check
+ json_add_object "firewall"
+ local http_open=0
+ local https_open=0
+ check_port_firewall_open 80 && http_open=1
+ check_port_firewall_open 443 && https_open=1
+
+ if [ "$http_open" = "1" ] && [ "$https_open" = "1" ]; then
+ json_add_string "status" "ok"
+ elif [ "$http_open" = "1" ] || [ "$https_open" = "1" ]; then
+ json_add_string "status" "partial"
+ else
+ json_add_string "status" "closed"
+ fi
+ json_add_boolean "http_open" "$http_open"
+ json_add_boolean "https_open" "$https_open"
+ json_close_object
+
+ # HAProxy status
+ json_add_object "haproxy"
+ if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then
+ json_add_string "status" "running"
+ else
+ json_add_string "status" "stopped"
+ fi
+ json_close_object
+
+ json_dump
+}
+
+# Batch health check for all published services (for dashboard)
+method_check_all_health() {
+ json_init
+ json_add_object "health"
+
+ local wan_ip
+ wan_ip=$(get_wan_ip)
+ json_add_string "wan_ip" "$wan_ip"
+
+ # Check HAProxy status
+ json_add_object "haproxy"
+ if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then
+ json_add_string "status" "running"
+ else
+ json_add_string "status" "stopped"
+ fi
+ json_close_object
+
+ # Check Tor status
+ json_add_object "tor"
+ if pgrep -f "/usr/sbin/tor" >/dev/null 2>&1; then
+ json_add_string "status" "running"
+ else
+ json_add_string "status" "stopped"
+ fi
+ json_close_object
+
+ # Check firewall ports
+ json_add_object "firewall"
+ local http_open=0
+ local https_open=0
+ check_port_firewall_open 80 && http_open=1
+ check_port_firewall_open 443 && https_open=1
+ json_add_boolean "http_open" "$http_open"
+ json_add_boolean "https_open" "$https_open"
+ if [ "$http_open" = "1" ] && [ "$https_open" = "1" ]; then
+ json_add_string "status" "ok"
+ elif [ "$http_open" = "1" ] || [ "$https_open" = "1" ]; then
+ json_add_string "status" "partial"
+ else
+ json_add_string "status" "closed"
+ fi
+ json_close_object
+
+ # Check individual services with domains
+ json_add_array "services"
+
+ # Get all published services with domains from HAProxy
+ local vhosts_json
+ vhosts_json=$(ubus call luci.haproxy list_vhosts 2>/dev/null)
+ if [ -n "$vhosts_json" ]; then
+ local count
+ count=$(echo "$vhosts_json" | jsonfilter -e '@.vhosts[*].domain' 2>/dev/null | wc -l)
+ local i=0
+ while [ $i -lt "$count" ]; do
+ local domain enabled
+ domain=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].domain" 2>/dev/null)
+ enabled=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].enabled" 2>/dev/null)
+
+ i=$((i + 1))
+
+ [ -z "$domain" ] && continue
+ [ "$enabled" = "false" ] || [ "$enabled" = "0" ] && continue
+
+ json_add_object
+ json_add_string "domain" "$domain"
+
+ # DNS check
+ local resolved_ip
+ resolved_ip=$(check_dns_resolution "$domain")
+ if [ -n "$resolved_ip" ]; then
+ json_add_string "dns_status" "ok"
+ json_add_string "dns_ip" "$resolved_ip"
+ else
+ json_add_string "dns_status" "failed"
+ fi
+
+ # Cert check
+ local days_left
+ days_left=$(check_cert_expiry "$domain")
+ if [ -n "$days_left" ]; then
+ if [ "$days_left" -lt 0 ]; then
+ json_add_string "cert_status" "expired"
+ elif [ "$days_left" -lt 7 ]; then
+ json_add_string "cert_status" "critical"
+ elif [ "$days_left" -lt 30 ]; then
+ json_add_string "cert_status" "warning"
+ else
+ json_add_string "cert_status" "ok"
+ fi
+ json_add_int "cert_days" "$days_left"
+ else
+ json_add_string "cert_status" "missing"
+ fi
+
+ json_close_object
+ done
+ fi
+
+ json_close_array
+ json_close_object
+
+ json_dump
+}
+
# Get certificate status for service
method_get_certificate_status() {
local service_id
@@ -908,13 +1207,27 @@ method_get_certificate_status() {
return
fi
- # Get certificate info from HAProxy
- local cert_info
- cert_info=$(ubus call luci.haproxy list_certificates 2>/dev/null)
+ # Get certificate expiry info
+ local days_left
+ days_left=$(check_cert_expiry "$haproxy_domain")
json_add_boolean "success" 1
json_add_string "domain" "$haproxy_domain"
- json_add_string "status" "unknown"
+
+ if [ -n "$days_left" ]; then
+ if [ "$days_left" -lt 0 ]; then
+ json_add_string "status" "expired"
+ elif [ "$days_left" -lt 7 ]; then
+ json_add_string "status" "critical"
+ elif [ "$days_left" -lt 30 ]; then
+ json_add_string "status" "warning"
+ else
+ json_add_string "status" "valid"
+ fi
+ json_add_int "days_left" "$days_left"
+ else
+ json_add_string "status" "missing"
+ fi
json_dump
}
@@ -978,6 +1291,8 @@ case "$1" in
"get_qr_data": { "service_id": "string", "url_type": "string" },
"list_categories": {},
"get_certificate_status": { "service_id": "string" },
+ "check_service_health": { "service_id": "string", "domain": "string" },
+ "check_all_health": {},
"get_landing_config": {},
"save_landing_config": { "auto_regen": "boolean" }
}
@@ -996,6 +1311,8 @@ EOF
get_qr_data) method_get_qr_data ;;
list_categories) method_list_categories ;;
get_certificate_status) method_get_certificate_status ;;
+ check_service_health) method_check_service_health ;;
+ check_all_health) method_check_all_health ;;
get_landing_config) method_get_landing_config ;;
save_landing_config) method_save_landing_config ;;
*)
diff --git a/package/secubox/luci-app-service-registry/root/usr/share/rpcd/acl.d/luci-app-service-registry.json b/package/secubox/luci-app-service-registry/root/usr/share/rpcd/acl.d/luci-app-service-registry.json
index a358fe13..f91234d3 100644
--- a/package/secubox/luci-app-service-registry/root/usr/share/rpcd/acl.d/luci-app-service-registry.json
+++ b/package/secubox/luci-app-service-registry/root/usr/share/rpcd/acl.d/luci-app-service-registry.json
@@ -9,6 +9,8 @@
"list_categories",
"get_qr_data",
"get_certificate_status",
+ "check_service_health",
+ "check_all_health",
"get_landing_config"
],
"luci.haproxy": [