diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/api.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/api.js index 3ccad39..cfca545 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/api.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/secubox-admin/api.js @@ -119,6 +119,13 @@ var callGetWidgetData = rpc.declare({ expect: { } }); +// Services Discovery +var callGetServices = rpc.declare({ + object: 'luci.secubox', + method: 'get_services', + expect: { services: [] } +}); + // ===== State Management API ===== var callGetComponentState = rpc.declare({ @@ -334,6 +341,9 @@ return baseclass.extend({ // Widget Data getWidgetData: debugRPC('getWidgetData', callGetWidgetData, { retries: 1 }), + // Services Discovery + getServices: debugRPC('getServices', callGetServices, { retries: 1 }), + // ===== State Management ===== getComponentState: debugRPC('getComponentState', callGetComponentState, { retries: 2 }), setComponentState: debugRPC('setComponentState', callSetComponentState, { retries: 1 }), diff --git a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js index abd22dd..7da4cbb 100644 --- a/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js +++ b/package/secubox/luci-app-secubox-admin/htdocs/luci-static/resources/view/secubox-admin/dashboard.js @@ -198,6 +198,70 @@ return view.extend({ ]); }, + renderServicesSection: function(services) { + if (!services || services.length === 0) { + return E('div', { 'class': 'services-section card' }, [ + E('h3', {}, '🔌 Active Services'), + E('p', { 'class': 'text-muted' }, 'No services detected') + ]); + } + + // Filter for external services only (accessible from network) + var externalServices = services.filter(function(s) { + return s.external && s.url; + }); + + // Group by category + var categories = {}; + externalServices.forEach(function(s) { + var cat = s.category || 'other'; + if (!categories[cat]) categories[cat] = []; + categories[cat].push(s); + }); + + var categoryOrder = ['app', 'monitoring', 'system', 'proxy', 'security', 'media', 'privacy', 'other']; + var categoryLabels = { + app: '📦 Applications', + monitoring: '📊 Monitoring', + system: '⚙️ System', + proxy: '🔀 Proxy', + security: '🛡️ Security', + media: '🎵 Media', + privacy: '🧅 Privacy', + other: '⚡ Other' + }; + + var serviceLinks = []; + categoryOrder.forEach(function(cat) { + if (categories[cat] && categories[cat].length > 0) { + categories[cat].forEach(function(svc) { + var url = window.location.protocol + '//' + window.location.hostname + svc.url; + serviceLinks.push(E('a', { + 'href': url, + 'target': '_blank', + 'class': 'service-link', + 'style': 'display:inline-flex;align-items:center;gap:8px;padding:10px 16px;' + + 'background:rgba(102,126,234,0.1);border:1px solid rgba(102,126,234,0.3);' + + 'border-radius:8px;text-decoration:none;color:#e0e0e0;font-size:14px;' + + 'transition:all 0.2s;margin:4px;' + }, [ + E('span', { 'style': 'font-size:18px;' }, svc.icon || '⚡'), + E('span', {}, svc.name), + E('span', { 'style': 'color:#888;font-size:12px;' }, ':' + svc.port) + ])); + }); + } + }); + + return E('div', { 'class': 'services-section card' }, [ + E('h3', {}, '🔌 Active Services'), + E('div', { 'class': 'services-grid', 'style': 'display:flex;flex-wrap:wrap;gap:8px;' }, + serviceLinks.length > 0 ? serviceLinks : + [E('p', { 'class': 'text-muted' }, 'No external services available')] + ) + ]); + }, + renderQuickActions: function() { return E('div', { 'class': 'quick-actions card' }, [ E('h3', {}, 'Quick Actions'), diff --git a/package/secubox/luci-app-secubox-admin/root/usr/share/rpcd/acl.d/luci-app-secubox-admin.json b/package/secubox/luci-app-secubox-admin/root/usr/share/rpcd/acl.d/luci-app-secubox-admin.json index 209f25c..cde88d9 100644 --- a/package/secubox/luci-app-secubox-admin/root/usr/share/rpcd/acl.d/luci-app-secubox-admin.json +++ b/package/secubox/luci-app-secubox-admin/root/usr/share/rpcd/acl.d/luci-app-secubox-admin.json @@ -18,7 +18,8 @@ "get_app_versions", "get_changelog", "get_widget_data", - "get_wan_access" + "get_wan_access", + "get_services" ] }, "uci": [ diff --git a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/view/secubox-portal/index.js b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/view/secubox-portal/index.js index fb2ddaf..4de9374 100644 --- a/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/view/secubox-portal/index.js +++ b/package/secubox/luci-app-secubox-portal/htdocs/luci-static/resources/view/secubox-portal/index.js @@ -23,6 +23,12 @@ var callCrowdSecStats = rpc.declare({ method: 'nftables_stats' }); +var callGetServices = rpc.declare({ + object: 'luci.secubox', + method: 'get_services', + expect: { services: [] } +}); + return view.extend({ currentSection: 'dashboard', appStatuses: {}, @@ -35,10 +41,14 @@ return view.extend({ callSystemInfo(), this.loadAppStatuses(), callCrowdSecStats().catch(function() { return null; }), - portal.checkInstalledApps() + portal.checkInstalledApps(), + callGetServices().catch(function() { return []; }) ]).then(function(results) { // Store installed apps info from the last promise self.installedApps = results[4] || {}; + // RPC expect unwraps the services array directly + var svcResult = results[5] || []; + self.detectedServices = Array.isArray(svcResult) ? svcResult : (svcResult.services || []); return results; }); }, @@ -407,9 +417,95 @@ return view.extend({ }, renderServicesSection: function() { - var apps = portal.getInstalledAppsBySection('services', this.installedApps); - return this.renderAppSection('services', 'Services', - 'Application services and server platforms', apps); + var self = this; + var services = this.detectedServices || []; + + // Filter for external services with URLs + var externalServices = services.filter(function(s) { + return s.external && s.url; + }); + + // Group by category + var categories = {}; + externalServices.forEach(function(s) { + var cat = s.category || 'other'; + if (!categories[cat]) categories[cat] = []; + categories[cat].push(s); + }); + + var categoryOrder = ['app', 'monitoring', 'system', 'proxy', 'security', 'media', 'privacy', 'other']; + var categoryLabels = { + app: '📦 Applications', + monitoring: '📊 Monitoring', + system: '⚙️ System', + proxy: '🔀 Proxy', + security: '🛡️ Security', + media: '🎵 Media', + privacy: '🧅 Privacy', + other: '⚡ Other' + }; + + var serviceCards = []; + + // Map icon names to emojis + var iconMap = { + 'lock': '🔐', 'globe': '🌐', 'arrow': '🔀', 'shield': '🔒', + 'git': '📦', 'blog': '📝', 'security': '🛡️', 'settings': '⚙️', + 'feed': '📡', 'chart': '📊', 'stats': '📈', 'admin': '🔧', + 'app': '🎨', 'music': '🎵', 'onion': '🧅', '': '⚡' + }; + + categoryOrder.forEach(function(cat) { + if (categories[cat] && categories[cat].length > 0) { + categories[cat].forEach(function(svc) { + var url = window.location.protocol + '//' + window.location.hostname + svc.url; + var emoji = iconMap[svc.icon] || '⚡'; + serviceCards.push(E('a', { + 'class': 'sb-app-card sb-service-card', + 'href': url, + 'target': '_blank' + }, [ + E('div', { 'class': 'sb-app-card-header' }, [ + E('div', { + 'class': 'sb-app-card-icon', + 'style': 'background: linear-gradient(135deg, #667eea, #764ba2); font-size: 24px;' + }, emoji), + E('div', {}, [ + E('h4', { 'class': 'sb-app-card-title' }, svc.name), + E('span', { 'class': 'sb-app-card-version' }, ':' + svc.port) + ]) + ]), + E('p', { 'class': 'sb-app-card-desc' }, categoryLabels[svc.category] || 'Service'), + E('div', { 'class': 'sb-app-card-status' }, [ + E('span', { 'class': 'sb-app-card-status-dot running' }), + E('span', { 'class': 'sb-app-card-status-text' }, 'Listening') + ]) + ])); + }); + } + }); + + if (serviceCards.length === 0) { + return E('div', { 'class': 'sb-portal-section', 'data-section': 'services' }, [ + E('div', { 'class': 'sb-section-header' }, [ + E('h2', { 'class': 'sb-section-title' }, '🔌 Active Services'), + E('p', { 'class': 'sb-section-subtitle' }, 'Detected services listening on network ports') + ]), + E('div', { 'class': 'sb-section-empty' }, [ + E('div', { 'class': 'sb-empty-icon' }, '🔌'), + E('p', { 'class': 'sb-empty-text' }, 'No external services detected'), + E('p', { 'class': 'sb-empty-hint' }, 'Services listening on localhost only are not shown') + ]) + ]); + } + + return E('div', { 'class': 'sb-portal-section', 'data-section': 'services' }, [ + E('div', { 'class': 'sb-section-header' }, [ + E('h2', { 'class': 'sb-section-title' }, '🔌 Active Services'), + E('p', { 'class': 'sb-section-subtitle' }, 'Detected services listening on network ports') + ]), + E('div', { 'class': 'sb-app-grid' }, serviceCards) + ]); }, renderAppSection: function(sectionId, title, subtitle, apps) { diff --git a/package/secubox/luci-app-secubox-portal/root/usr/share/rpcd/acl.d/luci-app-secubox-portal.json b/package/secubox/luci-app-secubox-portal/root/usr/share/rpcd/acl.d/luci-app-secubox-portal.json index 89cbb31..373f9c2 100644 --- a/package/secubox/luci-app-secubox-portal/root/usr/share/rpcd/acl.d/luci-app-secubox-portal.json +++ b/package/secubox/luci-app-secubox-portal/root/usr/share/rpcd/acl.d/luci-app-secubox-portal.json @@ -10,7 +10,8 @@ "getModuleInfo", "get_theme", "getStatus", - "getHealth" + "getHealth", + "get_services" ], "luci.system-hub": [ "status", diff --git a/package/secubox/secubox-app-hexojs/files/etc/config/hexojs b/package/secubox/secubox-app-hexojs/files/etc/config/hexojs index a29cf8f..e392d57 100644 --- a/package/secubox/secubox-app-hexojs/files/etc/config/hexojs +++ b/package/secubox/secubox-app-hexojs/files/etc/config/hexojs @@ -40,5 +40,5 @@ config theme_config 'theme' option logo_text 'Blog_' config portal 'portal' - option enabled '1' + option enabled '0' option path '/www' diff --git a/package/secubox/secubox-app-metabolizer/files/etc/config/metabolizer b/package/secubox/secubox-app-metabolizer/files/etc/config/metabolizer index 99ee324..9c003da 100644 --- a/package/secubox/secubox-app-metabolizer/files/etc/config/metabolizer +++ b/package/secubox/secubox-app-metabolizer/files/etc/config/metabolizer @@ -19,7 +19,7 @@ config hexo 'hexo' option source_path '/srv/hexojs/site/source/_posts' option public_path '/srv/hexojs/site/public' option portal_path '/www' - option auto_publish '1' + option auto_publish '0' config portal 'portal' option enabled '1' diff --git a/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox b/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox index 7de17b1..685cddb 100755 --- a/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox +++ b/package/secubox/secubox-core/root/usr/libexec/rpcd/luci.secubox @@ -221,6 +221,10 @@ case "$1" in json_add_object "apply_wan_access" json_close_object + # Services discovery + json_add_object "get_services" + json_close_object + json_dump ;; @@ -1174,6 +1178,70 @@ case "$1" in json_dump ;; + get_services) + # Discover listening services from netstat + # Save to temp file to avoid subshell issues with json + TMP_SERVICES="/tmp/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 -u > "$TMP_SERVICES" + + json_init + json_add_array "services" + + while read port local proc; do + addr=$(echo "$local" | sed 's/:[^:]*$//') + name="Service"; icon=""; category="other"; path="" + + case "$port" in + 22) name="SSH"; icon="lock"; category="system" ;; + 53) name="DNS"; icon="globe"; category="system" ;; + 80) name="HTTP"; icon="arrow"; path="/"; category="proxy" ;; + 443) name="HTTPS"; icon="shield"; path="/"; category="proxy" ;; + 3000) name="Gitea"; icon="git"; path=":3000"; category="app" ;; + 4000) name="HexoJS"; icon="blog"; path=":4000"; category="app" ;; + 8080) name="CrowdSec"; icon="security"; category="security" ;; + 8081) name="LuCI"; icon="settings"; path=":8081"; category="system" ;; + 8082) name="CyberFeed"; icon="feed"; path=":8082"; category="app" ;; + 8086) name="Netifyd"; icon="chart"; path=":8086"; category="monitoring" ;; + 8404) name="HAProxy Stats"; icon="stats"; path=":8404/stats"; category="monitoring" ;; + 8444) name="LuCI HTTPS"; icon="admin"; path=":8444"; category="system" ;; + 8501) name="Streamlit"; icon="app"; path=":8501"; category="app" ;; + 9000) name="Lyrion"; icon="music"; path=":9000"; category="media" ;; + 9050) name="Tor SOCKS"; icon="onion"; category="privacy" ;; + 9090) name="Lyrion CLI"; icon="music"; category="media" ;; + 2222) name="Gitea SSH"; icon="git"; category="app" ;; + 3483) name="Squeezebox"; icon="music"; category="media" ;; + esac + + external=0 + case "$addr" in 0.0.0.0|::) external=1 ;; 127.0.0.1|::1) ;; *) external=1 ;; esac + + json_add_object "" + json_add_int "port" "$port" + json_add_string "address" "$addr" + json_add_string "name" "$name" + json_add_string "icon" "$icon" + json_add_string "process" "$proc" + json_add_string "category" "$category" + json_add_boolean "external" "$external" + [ -n "$path" ] && [ "$external" = "1" ] && json_add_string "url" "$path" + json_close_object + done < "$TMP_SERVICES" + + rm -f "$TMP_SERVICES" + json_close_array + json_dump + ;; + + *) json_init json_add_boolean "error" true