commit
585b7f0f2f
@ -119,6 +119,13 @@ var callGetWidgetData = rpc.declare({
|
|||||||
expect: { }
|
expect: { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Services Discovery
|
||||||
|
var callGetServices = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'get_services',
|
||||||
|
expect: { services: [] }
|
||||||
|
});
|
||||||
|
|
||||||
// ===== State Management API =====
|
// ===== State Management API =====
|
||||||
|
|
||||||
var callGetComponentState = rpc.declare({
|
var callGetComponentState = rpc.declare({
|
||||||
@ -334,6 +341,9 @@ return baseclass.extend({
|
|||||||
// Widget Data
|
// Widget Data
|
||||||
getWidgetData: debugRPC('getWidgetData', callGetWidgetData, { retries: 1 }),
|
getWidgetData: debugRPC('getWidgetData', callGetWidgetData, { retries: 1 }),
|
||||||
|
|
||||||
|
// Services Discovery
|
||||||
|
getServices: debugRPC('getServices', callGetServices, { retries: 1 }),
|
||||||
|
|
||||||
// ===== State Management =====
|
// ===== State Management =====
|
||||||
getComponentState: debugRPC('getComponentState', callGetComponentState, { retries: 2 }),
|
getComponentState: debugRPC('getComponentState', callGetComponentState, { retries: 2 }),
|
||||||
setComponentState: debugRPC('setComponentState', callSetComponentState, { retries: 1 }),
|
setComponentState: debugRPC('setComponentState', callSetComponentState, { retries: 1 }),
|
||||||
|
|||||||
@ -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() {
|
renderQuickActions: function() {
|
||||||
return E('div', { 'class': 'quick-actions card' }, [
|
return E('div', { 'class': 'quick-actions card' }, [
|
||||||
E('h3', {}, 'Quick Actions'),
|
E('h3', {}, 'Quick Actions'),
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
"get_app_versions",
|
"get_app_versions",
|
||||||
"get_changelog",
|
"get_changelog",
|
||||||
"get_widget_data",
|
"get_widget_data",
|
||||||
"get_wan_access"
|
"get_wan_access",
|
||||||
|
"get_services"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uci": [
|
"uci": [
|
||||||
|
|||||||
@ -23,6 +23,12 @@ var callCrowdSecStats = rpc.declare({
|
|||||||
method: 'nftables_stats'
|
method: 'nftables_stats'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callGetServices = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'get_services',
|
||||||
|
expect: { services: [] }
|
||||||
|
});
|
||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
currentSection: 'dashboard',
|
currentSection: 'dashboard',
|
||||||
appStatuses: {},
|
appStatuses: {},
|
||||||
@ -35,10 +41,14 @@ return view.extend({
|
|||||||
callSystemInfo(),
|
callSystemInfo(),
|
||||||
this.loadAppStatuses(),
|
this.loadAppStatuses(),
|
||||||
callCrowdSecStats().catch(function() { return null; }),
|
callCrowdSecStats().catch(function() { return null; }),
|
||||||
portal.checkInstalledApps()
|
portal.checkInstalledApps(),
|
||||||
|
callGetServices().catch(function() { return []; })
|
||||||
]).then(function(results) {
|
]).then(function(results) {
|
||||||
// Store installed apps info from the last promise
|
// Store installed apps info from the last promise
|
||||||
self.installedApps = results[4] || {};
|
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;
|
return results;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -407,9 +417,95 @@ return view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderServicesSection: function() {
|
renderServicesSection: function() {
|
||||||
var apps = portal.getInstalledAppsBySection('services', this.installedApps);
|
var self = this;
|
||||||
return this.renderAppSection('services', 'Services',
|
var services = this.detectedServices || [];
|
||||||
'Application services and server platforms', apps);
|
|
||||||
|
// 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) {
|
renderAppSection: function(sectionId, title, subtitle, apps) {
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
"getModuleInfo",
|
"getModuleInfo",
|
||||||
"get_theme",
|
"get_theme",
|
||||||
"getStatus",
|
"getStatus",
|
||||||
"getHealth"
|
"getHealth",
|
||||||
|
"get_services"
|
||||||
],
|
],
|
||||||
"luci.system-hub": [
|
"luci.system-hub": [
|
||||||
"status",
|
"status",
|
||||||
|
|||||||
@ -40,5 +40,5 @@ config theme_config 'theme'
|
|||||||
option logo_text 'Blog_'
|
option logo_text 'Blog_'
|
||||||
|
|
||||||
config portal 'portal'
|
config portal 'portal'
|
||||||
option enabled '1'
|
option enabled '0'
|
||||||
option path '/www'
|
option path '/www'
|
||||||
|
|||||||
@ -19,7 +19,7 @@ config hexo 'hexo'
|
|||||||
option source_path '/srv/hexojs/site/source/_posts'
|
option source_path '/srv/hexojs/site/source/_posts'
|
||||||
option public_path '/srv/hexojs/site/public'
|
option public_path '/srv/hexojs/site/public'
|
||||||
option portal_path '/www'
|
option portal_path '/www'
|
||||||
option auto_publish '1'
|
option auto_publish '0'
|
||||||
|
|
||||||
config portal 'portal'
|
config portal 'portal'
|
||||||
option enabled '1'
|
option enabled '1'
|
||||||
|
|||||||
@ -221,6 +221,10 @@ case "$1" in
|
|||||||
json_add_object "apply_wan_access"
|
json_add_object "apply_wan_access"
|
||||||
json_close_object
|
json_close_object
|
||||||
|
|
||||||
|
# Services discovery
|
||||||
|
json_add_object "get_services"
|
||||||
|
json_close_object
|
||||||
|
|
||||||
json_dump
|
json_dump
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@ -1174,6 +1178,70 @@ case "$1" in
|
|||||||
json_dump
|
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_init
|
||||||
json_add_boolean "error" true
|
json_add_boolean "error" true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user