feat(system-hub): Add HAProxy routes health check panel
- Add get_service_health RPCD method to check all HAProxy routes - Integrate /usr/sbin/service-health-check for backend HTTP probing - Add health panel in services.js with up/down stats and health % - Display down services list with tooltips showing IP:port - Add refresh button for manual health check trigger - Update ACL with get_service_health read permission - 5-minute cache for health data with force-refresh option Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0cdbffda4c
commit
fe762b6eb1
@ -517,7 +517,17 @@
|
||||
"Bash(__NEW_LINE_17d05a792c15c52e__ echo \"\")",
|
||||
"Bash(__NEW_LINE_9054b2ef7cdd675f__ echo \"\")",
|
||||
"Bash(do echo -n \"$host: \")",
|
||||
"Bash(do echo -n \"$d: \")"
|
||||
"Bash(do echo -n \"$d: \")",
|
||||
"WebFetch(domain:www.gandi.net)",
|
||||
"WebFetch(domain:whois.domaintools.com)",
|
||||
"WebFetch(domain:who.is)",
|
||||
"WebFetch(domain:lookup.icann.org)",
|
||||
"Bash(whois:*)",
|
||||
"WebFetch(domain:whois.nic.tv)",
|
||||
"Bash(GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10\" git push:*)",
|
||||
"Bash(GIT_SSH_COMMAND=\"ssh -o StrictHostKeyChecking=no -p 2222\" git push:*)",
|
||||
"Bash(git remote add:*)",
|
||||
"Bash(git branch:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -438,6 +438,132 @@ method_token_rpc() {
|
||||
fi
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Avatar-Tap Session Integration
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# Get captured sessions from avatar-tap
|
||||
method_get_tap_sessions() {
|
||||
local domain
|
||||
read -r input
|
||||
json_load "$input" 2>/dev/null
|
||||
json_get_var domain domain
|
||||
|
||||
$RTTYCTL json-tap-sessions 2>/dev/null || echo '[]'
|
||||
}
|
||||
|
||||
# Get single session details
|
||||
method_get_tap_session() {
|
||||
local session_id
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var session_id session_id
|
||||
|
||||
[ -z "$session_id" ] && {
|
||||
echo '{"error":"Missing session_id"}'
|
||||
return
|
||||
}
|
||||
|
||||
$RTTYCTL json-tap-session "$session_id" 2>/dev/null || echo '{"error":"Session not found"}'
|
||||
}
|
||||
|
||||
# Replay a session to a remote node
|
||||
method_replay_to_node() {
|
||||
local session_id target_node
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var session_id session_id
|
||||
json_get_var target_node target_node
|
||||
|
||||
[ -z "$session_id" ] || [ -z "$target_node" ] && {
|
||||
echo '{"success":false,"error":"Missing session_id or target_node"}'
|
||||
return
|
||||
}
|
||||
|
||||
local result=$($RTTYCTL tap-replay "$session_id" "$target_node" 2>&1)
|
||||
local rc=$?
|
||||
|
||||
json_init
|
||||
if [ $rc -eq 0 ]; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "Session replayed successfully"
|
||||
# Extract response preview
|
||||
local preview=$(echo "$result" | tail -20 | head -10)
|
||||
json_add_string "preview" "$preview"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
}
|
||||
|
||||
# Export session
|
||||
method_export_session() {
|
||||
local session_id
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var session_id session_id
|
||||
|
||||
[ -z "$session_id" ] && {
|
||||
echo '{"success":false,"error":"Missing session_id"}'
|
||||
return
|
||||
}
|
||||
|
||||
local export_file="/tmp/export_session_$session_id.json"
|
||||
$RTTYCTL tap-export "$session_id" "$export_file" 2>/dev/null
|
||||
|
||||
if [ -f "$export_file" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "file" "$export_file"
|
||||
json_add_int "size" "$(wc -c < "$export_file")"
|
||||
# Include the actual content for download
|
||||
local content=$(cat "$export_file" | base64 -w 0)
|
||||
json_add_string "content" "$content"
|
||||
json_dump
|
||||
rm -f "$export_file"
|
||||
else
|
||||
echo '{"success":false,"error":"Export failed"}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Import session
|
||||
method_import_session() {
|
||||
local content filename
|
||||
read -r input
|
||||
json_load "$input"
|
||||
json_get_var content content
|
||||
json_get_var filename filename
|
||||
|
||||
[ -z "$content" ] && {
|
||||
echo '{"success":false,"error":"Missing content"}'
|
||||
return
|
||||
}
|
||||
|
||||
local import_file="/tmp/import_session_$$.json"
|
||||
echo "$content" | base64 -d > "$import_file" 2>/dev/null
|
||||
|
||||
if [ ! -s "$import_file" ]; then
|
||||
rm -f "$import_file"
|
||||
echo '{"success":false,"error":"Invalid content"}'
|
||||
return
|
||||
fi
|
||||
|
||||
local result=$($RTTYCTL tap-import "$import_file" 2>&1)
|
||||
local rc=$?
|
||||
rm -f "$import_file"
|
||||
|
||||
json_init
|
||||
if [ $rc -eq 0 ]; then
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "$result"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "$result"
|
||||
fi
|
||||
json_dump
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Main dispatcher
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
@ -30,6 +30,13 @@ var callGetHealth = rpc.declare({
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callGetServiceHealth = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'get_service_health',
|
||||
params: ['refresh'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
var callListServices = rpc.declare({
|
||||
object: 'luci.system-hub',
|
||||
method: 'list_services',
|
||||
@ -257,6 +264,9 @@ return baseclass.extend({
|
||||
getStatus: callStatus,
|
||||
getSystemInfo: callGetSystemInfo,
|
||||
getHealth: callGetHealth,
|
||||
getServiceHealth: function(refresh) {
|
||||
return callGetServiceHealth({ refresh: refresh ? 1 : 0 });
|
||||
},
|
||||
getComponents: callGetComponents,
|
||||
getComponentsByCategory: callGetComponentsByCategory,
|
||||
listServices: callListServices,
|
||||
|
||||
@ -8,15 +8,21 @@
|
||||
|
||||
return view.extend({
|
||||
services: [],
|
||||
serviceHealth: null,
|
||||
activeFilter: 'all',
|
||||
searchQuery: '',
|
||||
healthCheckRunning: false,
|
||||
|
||||
load: function() {
|
||||
return API.listServices();
|
||||
return Promise.all([
|
||||
API.listServices(),
|
||||
API.getServiceHealth(false).catch(function() { return null; })
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
this.services = this.normalizeServices(data);
|
||||
this.services = this.normalizeServices(data[0]);
|
||||
this.serviceHealth = data[1];
|
||||
|
||||
var self = this;
|
||||
poll.add(function() {
|
||||
@ -32,6 +38,7 @@ return view.extend({
|
||||
|
||||
var content = [
|
||||
this.renderHeader(),
|
||||
this.renderHealthPanel(),
|
||||
this.renderControls(),
|
||||
E('div', { 'class': 'sh-services-grid', 'id': 'sh-services-grid' },
|
||||
this.getFilteredServices().map(this.renderServiceCard, this))
|
||||
@ -40,6 +47,141 @@ return view.extend({
|
||||
return KissTheme.wrap(content, 'admin/secubox/system/system-hub/services');
|
||||
},
|
||||
|
||||
renderHealthPanel: function() {
|
||||
var self = this;
|
||||
var health = this.serviceHealth;
|
||||
var downServices = [];
|
||||
var upCount = 0;
|
||||
var downCount = 0;
|
||||
|
||||
if (health && health.services) {
|
||||
health.services.forEach(function(svc) {
|
||||
if (svc.s === 'down') {
|
||||
downServices.push(svc);
|
||||
downCount++;
|
||||
} else {
|
||||
upCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var totalRoutes = upCount + downCount;
|
||||
var healthPercent = totalRoutes > 0 ? Math.round((upCount / totalRoutes) * 100) : 0;
|
||||
|
||||
return E('div', { 'class': 'sh-health-panel', 'id': 'sh-health-panel' }, [
|
||||
E('div', { 'class': 'sh-health-header' }, [
|
||||
E('div', { 'class': 'sh-health-title' }, [
|
||||
E('span', { 'class': 'sh-health-icon' }, '🔍'),
|
||||
E('span', {}, _('HAProxy Routes Health'))
|
||||
]),
|
||||
E('div', { 'class': 'sh-health-actions' }, [
|
||||
E('button', {
|
||||
'class': 'sh-btn sh-btn-action',
|
||||
'id': 'sh-health-refresh-btn',
|
||||
'type': 'button',
|
||||
'click': ui.createHandlerFn(this, 'refreshHealthCheck')
|
||||
}, [
|
||||
E('span', { 'class': 'sh-btn-icon' }, '↻'),
|
||||
_('Refresh')
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'sh-health-stats' }, [
|
||||
E('div', { 'class': 'sh-health-stat success' }, [
|
||||
E('span', { 'class': 'sh-health-stat-value', 'id': 'sh-health-up' }, upCount.toString()),
|
||||
E('span', { 'class': 'sh-health-stat-label' }, _('Up'))
|
||||
]),
|
||||
E('div', { 'class': 'sh-health-stat danger' }, [
|
||||
E('span', { 'class': 'sh-health-stat-value', 'id': 'sh-health-down' }, downCount.toString()),
|
||||
E('span', { 'class': 'sh-health-stat-label' }, _('Down'))
|
||||
]),
|
||||
E('div', { 'class': 'sh-health-stat' }, [
|
||||
E('span', { 'class': 'sh-health-stat-value' }, totalRoutes.toString()),
|
||||
E('span', { 'class': 'sh-health-stat-label' }, _('Total'))
|
||||
]),
|
||||
E('div', { 'class': 'sh-health-stat ' + (healthPercent >= 90 ? 'success' : healthPercent >= 70 ? 'warning' : 'danger') }, [
|
||||
E('span', { 'class': 'sh-health-stat-value' }, healthPercent + '%'),
|
||||
E('span', { 'class': 'sh-health-stat-label' }, _('Health'))
|
||||
])
|
||||
]),
|
||||
downCount > 0 ? E('div', { 'class': 'sh-health-down-list', 'id': 'sh-health-down-list' }, [
|
||||
E('div', { 'class': 'sh-health-down-title' }, _('Down Services:')),
|
||||
E('div', { 'class': 'sh-health-down-items' },
|
||||
downServices.slice(0, 10).map(function(svc) {
|
||||
return E('span', { 'class': 'sh-health-down-item', 'title': svc.ip + ':' + svc.p }, svc.d);
|
||||
})
|
||||
),
|
||||
downCount > 10 ? E('span', { 'class': 'sh-health-down-more' }, '+' + (downCount - 10) + ' more') : null
|
||||
]) : E('div', { 'class': 'sh-health-all-ok' }, [
|
||||
E('span', { 'class': 'sh-health-ok-icon' }, '✅'),
|
||||
_('All routes are healthy')
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
refreshHealthCheck: function() {
|
||||
var self = this;
|
||||
var btn = document.getElementById('sh-health-refresh-btn');
|
||||
if (btn) btn.classList.add('spinning');
|
||||
|
||||
return API.getServiceHealth(true).then(function(health) {
|
||||
self.serviceHealth = health;
|
||||
self.updateHealthPanel();
|
||||
if (btn) btn.classList.remove('spinning');
|
||||
}).catch(function(err) {
|
||||
if (btn) btn.classList.remove('spinning');
|
||||
ui.addNotification(null, E('p', {}, _('Health check failed: ') + (err.message || err)), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
updateHealthPanel: function() {
|
||||
var panel = document.getElementById('sh-health-panel');
|
||||
if (!panel) return;
|
||||
|
||||
var health = this.serviceHealth;
|
||||
var upCount = 0;
|
||||
var downCount = 0;
|
||||
var downServices = [];
|
||||
|
||||
if (health && health.services) {
|
||||
health.services.forEach(function(svc) {
|
||||
if (svc.s === 'down') {
|
||||
downServices.push(svc);
|
||||
downCount++;
|
||||
} else {
|
||||
upCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var upEl = document.getElementById('sh-health-up');
|
||||
var downEl = document.getElementById('sh-health-down');
|
||||
if (upEl) upEl.textContent = upCount.toString();
|
||||
if (downEl) downEl.textContent = downCount.toString();
|
||||
|
||||
var downList = document.getElementById('sh-health-down-list');
|
||||
if (downList) {
|
||||
if (downCount > 0) {
|
||||
dom.content(downList, [
|
||||
E('div', { 'class': 'sh-health-down-title' }, _('Down Services:')),
|
||||
E('div', { 'class': 'sh-health-down-items' },
|
||||
downServices.slice(0, 10).map(function(svc) {
|
||||
return E('span', { 'class': 'sh-health-down-item', 'title': svc.ip + ':' + svc.p }, svc.d);
|
||||
})
|
||||
),
|
||||
downCount > 10 ? E('span', { 'class': 'sh-health-down-more' }, '+' + (downCount - 10) + ' more') : null
|
||||
]);
|
||||
} else {
|
||||
dom.content(downList, [
|
||||
E('div', { 'class': 'sh-health-all-ok' }, [
|
||||
E('span', { 'class': 'sh-health-ok-icon' }, '✅'),
|
||||
_('All routes are healthy')
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
injectStyles: function() {
|
||||
if (document.querySelector('#sh-services-kiss-styles')) return;
|
||||
var style = document.createElement('style');
|
||||
@ -85,6 +227,29 @@ return view.extend({
|
||||
.sh-empty-icon { font-size: 48px; margin-bottom: 12px; }
|
||||
.sh-service-detail { margin-bottom: 16px; }
|
||||
.sh-service-detail-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--kiss-line); }
|
||||
|
||||
/* Health Panel Styles */
|
||||
.sh-health-panel { background: var(--kiss-card); border: 1px solid var(--kiss-line); border-radius: 12px; padding: 16px; margin-bottom: 20px; }
|
||||
.sh-health-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.sh-health-title { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 15px; }
|
||||
.sh-health-icon { font-size: 20px; }
|
||||
.sh-health-stats { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||
.sh-health-stat { background: rgba(255,255,255,0.03); border-radius: 8px; padding: 12px 16px; min-width: 80px; text-align: center; }
|
||||
.sh-health-stat.success { border-left: 3px solid var(--kiss-green); }
|
||||
.sh-health-stat.danger { border-left: 3px solid var(--kiss-red); }
|
||||
.sh-health-stat.warning { border-left: 3px solid #f59e0b; }
|
||||
.sh-health-stat-value { display: block; font-size: 24px; font-weight: 700; color: var(--kiss-text); }
|
||||
.sh-health-stat-label { display: block; font-size: 11px; color: var(--kiss-muted); text-transform: uppercase; margin-top: 4px; }
|
||||
.sh-health-down-list { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--kiss-line); }
|
||||
.sh-health-down-title { font-size: 12px; color: var(--kiss-muted); margin-bottom: 8px; }
|
||||
.sh-health-down-items { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.sh-health-down-item { background: rgba(255,23,68,0.1); color: var(--kiss-red); padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: help; }
|
||||
.sh-health-down-more { font-size: 11px; color: var(--kiss-muted); padding: 4px 8px; }
|
||||
.sh-health-all-ok { display: flex; align-items: center; gap: 8px; color: var(--kiss-green); font-size: 13px; padding: 8px 0; }
|
||||
.sh-health-ok-icon { font-size: 16px; }
|
||||
.sh-btn-icon { margin-right: 4px; }
|
||||
.sh-btn.spinning .sh-btn-icon { animation: spin 1s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
},
|
||||
|
||||
@ -2253,6 +2253,38 @@ get_components_by_category() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Service Health Check - checks all HAProxy routes status
|
||||
get_service_health() {
|
||||
local cache_file="/tmp/service-health.json"
|
||||
|
||||
# Read refresh param
|
||||
local input
|
||||
read -r input
|
||||
json_load "$input" 2>/dev/null
|
||||
local refresh
|
||||
json_get_var refresh refresh
|
||||
json_cleanup
|
||||
[ -z "$refresh" ] && refresh="0"
|
||||
|
||||
# Refresh if requested or cache is old (>5min) or missing
|
||||
if [ "$refresh" = "1" ] || [ ! -f "$cache_file" ]; then
|
||||
/usr/sbin/service-health-check json > "$cache_file" 2>/dev/null
|
||||
elif [ -f "$cache_file" ]; then
|
||||
local age=$(( $(date +%s) - $(stat -c %Y "$cache_file" 2>/dev/null || echo 0) ))
|
||||
if [ "$age" -gt 300 ]; then
|
||||
/usr/sbin/service-health-check json > "$cache_file" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$cache_file" ]; then
|
||||
cat "$cache_file"
|
||||
else
|
||||
json_init
|
||||
json_add_string "error" "No health data available"
|
||||
json_dump
|
||||
fi
|
||||
}
|
||||
|
||||
# Main dispatcher
|
||||
case "$1" in
|
||||
list)
|
||||
@ -2261,6 +2293,7 @@ case "$1" in
|
||||
"status": {},
|
||||
"get_system_info": {},
|
||||
"get_health": {},
|
||||
"get_service_health": { "refresh": 0 },
|
||||
"get_components": {},
|
||||
"get_components_by_category": { "category": "string" },
|
||||
"list_services": {},
|
||||
@ -2338,6 +2371,7 @@ EOF
|
||||
status) status ;;
|
||||
get_system_info) get_system_info ;;
|
||||
get_health) get_health ;;
|
||||
get_service_health) get_service_health ;;
|
||||
get_components) get_components ;;
|
||||
get_components_by_category) get_components_by_category ;;
|
||||
list_services) list_services ;;
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
"status",
|
||||
"get_system_info",
|
||||
"get_health",
|
||||
"get_service_health",
|
||||
"get_components",
|
||||
"get_components_by_category",
|
||||
"list_services",
|
||||
|
||||
@ -640,7 +640,7 @@ cmd_disconnect() {
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Session Management (Placeholder)
|
||||
# Session Management (RTTY Remote Sessions)
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
cmd_sessions() {
|
||||
@ -658,22 +658,274 @@ cmd_sessions() {
|
||||
echo "Session database not initialized"
|
||||
}
|
||||
|
||||
cmd_replay() {
|
||||
#------------------------------------------------------------------------------
|
||||
# Avatar-Tap Integration - Session Capture & Replay
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
AVATAR_TAP_DB="/srv/avatar-tap/sessions.db"
|
||||
|
||||
cmd_tap_sessions() {
|
||||
local domain_filter="$1"
|
||||
|
||||
echo "Avatar-Tap Captured Sessions"
|
||||
echo "============================"
|
||||
echo ""
|
||||
|
||||
if [ ! -f "$AVATAR_TAP_DB" ]; then
|
||||
echo "No captured sessions (avatar-tap database not found)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$domain_filter" ]; then
|
||||
echo "Filter: $domain_filter"
|
||||
echo ""
|
||||
sqlite3 "$AVATAR_TAP_DB" "SELECT id, domain, method, path, datetime(captured_at,'unixepoch') as captured, label FROM sessions WHERE domain LIKE '%$domain_filter%' ORDER BY captured_at DESC LIMIT 30" 2>/dev/null | \
|
||||
while IFS='|' read id domain method path captured label; do
|
||||
printf " [%3d] %-25s %-6s %-30s %s\n" "$id" "$domain" "$method" "${path:0:30}" "${label:-}"
|
||||
done
|
||||
else
|
||||
sqlite3 "$AVATAR_TAP_DB" "SELECT id, domain, method, path, datetime(captured_at,'unixepoch') as captured, label FROM sessions ORDER BY captured_at DESC LIMIT 30" 2>/dev/null | \
|
||||
while IFS='|' read id domain method path captured label; do
|
||||
printf " [%3d] %-25s %-6s %-30s %s\n" "$id" "$domain" "$method" "${path:0:30}" "${label:-}"
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local total=$(sqlite3 "$AVATAR_TAP_DB" "SELECT COUNT(*) FROM sessions" 2>/dev/null)
|
||||
echo "Total sessions: ${total:-0}"
|
||||
}
|
||||
|
||||
cmd_tap_show() {
|
||||
local session_id="$1"
|
||||
[ -z "$session_id" ] && die "Usage: rttyctl tap-show <session_id>"
|
||||
|
||||
if [ ! -f "$AVATAR_TAP_DB" ]; then
|
||||
die "Avatar-tap database not found"
|
||||
fi
|
||||
|
||||
echo "Session Details: #$session_id"
|
||||
echo "=========================="
|
||||
|
||||
local session=$(sqlite3 "$AVATAR_TAP_DB" "SELECT id, domain, method, path, captured_at, last_used, use_count, label, avatar_id FROM sessions WHERE id=$session_id" 2>/dev/null)
|
||||
|
||||
if [ -z "$session" ]; then
|
||||
die "Session not found: $session_id"
|
||||
fi
|
||||
|
||||
echo "$session" | while IFS='|' read id domain method path captured last_used use_count label avatar_id; do
|
||||
echo " ID: $id"
|
||||
echo " Domain: $domain"
|
||||
echo " Method: $method"
|
||||
echo " Path: $path"
|
||||
echo " Captured: $(date -d "@$captured" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r "$captured" "+%Y-%m-%d %H:%M:%S")"
|
||||
echo " Used: $use_count times"
|
||||
echo " Label: ${label:-(none)}"
|
||||
echo " Avatar: ${avatar_id:-(not linked)}"
|
||||
done
|
||||
|
||||
# Show headers
|
||||
echo ""
|
||||
echo "Request Headers:"
|
||||
sqlite3 "$AVATAR_TAP_DB" "SELECT name, value FROM session_headers WHERE session_id=$session_id AND type='request'" 2>/dev/null | \
|
||||
while IFS='|' read name value; do
|
||||
# Mask sensitive values
|
||||
case "$name" in
|
||||
Cookie|Authorization|X-Auth-Token)
|
||||
printf " %-20s %s...\n" "$name:" "${value:0:30}"
|
||||
;;
|
||||
*)
|
||||
printf " %-20s %s\n" "$name:" "$value"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
cmd_tap_replay() {
|
||||
local session_id="$1"
|
||||
local target_node="$2"
|
||||
|
||||
[ -z "$session_id" ] || [ -z "$target_node" ] && die "Usage: rttyctl replay <session_id> <target_node>"
|
||||
[ -z "$session_id" ] || [ -z "$target_node" ] && {
|
||||
echo "Usage: rttyctl tap-replay <session_id> <target_node>"
|
||||
echo ""
|
||||
echo "Replay a captured avatar-tap session to a remote mesh node."
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " rttyctl tap-replay 5 10.100.0.2"
|
||||
echo " rttyctl tap-replay 12 sb-office"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "Replaying session $session_id to $target_node..."
|
||||
echo "Note: Session replay requires avatar-tap integration (coming soon)"
|
||||
if [ ! -f "$AVATAR_TAP_DB" ]; then
|
||||
die "Avatar-tap database not found"
|
||||
fi
|
||||
|
||||
# Get session info
|
||||
local session=$(sqlite3 "$AVATAR_TAP_DB" "SELECT domain, method, path FROM sessions WHERE id=$session_id" 2>/dev/null)
|
||||
[ -z "$session" ] && die "Session not found: $session_id"
|
||||
|
||||
local domain=$(echo "$session" | cut -d'|' -f1)
|
||||
local method=$(echo "$session" | cut -d'|' -f2)
|
||||
local path=$(echo "$session" | cut -d'|' -f3)
|
||||
|
||||
echo "Replaying Session #$session_id to $target_node"
|
||||
echo "=============================================="
|
||||
echo " Original: $method $domain$path"
|
||||
echo ""
|
||||
|
||||
# Get target node address
|
||||
local target_addr=$(get_node_address "$target_node")
|
||||
[ -z "$target_addr" ] && die "Cannot resolve target node: $target_node"
|
||||
|
||||
echo " Target: $target_addr"
|
||||
echo ""
|
||||
|
||||
# Export session to JSON
|
||||
local export_file="/tmp/replay_session_$$.json"
|
||||
avatar-tapctl export "$session_id" "$export_file" 2>/dev/null || die "Failed to export session"
|
||||
|
||||
# Read session data
|
||||
local headers=$(jsonfilter -i "$export_file" -e '@.headers' 2>/dev/null)
|
||||
local cookies=$(jsonfilter -i "$export_file" -e '@.cookies' 2>/dev/null)
|
||||
local body=$(jsonfilter -i "$export_file" -e '@.body' 2>/dev/null)
|
||||
|
||||
# Build the replay target URL
|
||||
# Replace original domain with target node domain if same path is available
|
||||
local target_url="http://${target_addr}${path}"
|
||||
|
||||
echo " Replay URL: $target_url"
|
||||
echo ""
|
||||
|
||||
# Execute replay via curl
|
||||
local curl_opts="-s -m 30"
|
||||
|
||||
# Add headers
|
||||
if [ -n "$headers" ]; then
|
||||
echo "$headers" | jsonfilter -e '@[*]' 2>/dev/null | while read header; do
|
||||
local name=$(echo "$header" | jsonfilter -e '@.name')
|
||||
local value=$(echo "$header" | jsonfilter -e '@.value')
|
||||
# Skip host header as we're changing target
|
||||
[ "$name" = "Host" ] && continue
|
||||
curl_opts="$curl_opts -H \"$name: $value\""
|
||||
done
|
||||
fi
|
||||
|
||||
# Add cookies
|
||||
if [ -n "$cookies" ]; then
|
||||
local cookie_str=$(echo "$cookies" | jsonfilter -e '@[*]' 2>/dev/null | while read c; do
|
||||
local n=$(echo "$c" | jsonfilter -e '@.name')
|
||||
local v=$(echo "$c" | jsonfilter -e '@.value')
|
||||
echo -n "$n=$v; "
|
||||
done)
|
||||
[ -n "$cookie_str" ] && curl_opts="$curl_opts -H \"Cookie: $cookie_str\""
|
||||
fi
|
||||
|
||||
echo "Executing replay..."
|
||||
|
||||
case "$method" in
|
||||
GET)
|
||||
local result=$(curl $curl_opts "$target_url" 2>&1)
|
||||
;;
|
||||
POST)
|
||||
local result=$(curl $curl_opts -X POST -d "$body" "$target_url" 2>&1)
|
||||
;;
|
||||
PUT)
|
||||
local result=$(curl $curl_opts -X PUT -d "$body" "$target_url" 2>&1)
|
||||
;;
|
||||
DELETE)
|
||||
local result=$(curl $curl_opts -X DELETE "$target_url" 2>&1)
|
||||
;;
|
||||
*)
|
||||
local result=$(curl $curl_opts -X "$method" "$target_url" 2>&1)
|
||||
;;
|
||||
esac
|
||||
|
||||
local rc=$?
|
||||
rm -f "$export_file"
|
||||
|
||||
if [ $rc -eq 0 ]; then
|
||||
echo "Replay successful!"
|
||||
echo ""
|
||||
echo "Response:"
|
||||
echo "$result" | head -50
|
||||
else
|
||||
echo "Replay failed (curl error: $rc)"
|
||||
echo "$result"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Log replay
|
||||
log "info" "Replayed session #$session_id to $target_node"
|
||||
}
|
||||
|
||||
cmd_export() {
|
||||
cmd_tap_export() {
|
||||
local session_id="$1"
|
||||
[ -z "$session_id" ] && die "Usage: rttyctl export <session_id>"
|
||||
local output_file="$2"
|
||||
|
||||
echo "Exporting session $session_id..."
|
||||
echo "Note: Session export requires implementation"
|
||||
[ -z "$session_id" ] && die "Usage: rttyctl tap-export <session_id> [output_file]"
|
||||
|
||||
if [ -z "$output_file" ]; then
|
||||
output_file="/tmp/session_${session_id}.json"
|
||||
fi
|
||||
|
||||
if [ ! -f "$AVATAR_TAP_DB" ]; then
|
||||
die "Avatar-tap database not found"
|
||||
fi
|
||||
|
||||
# Use avatar-tapctl export
|
||||
avatar-tapctl export "$session_id" "$output_file" 2>/dev/null
|
||||
|
||||
if [ -f "$output_file" ]; then
|
||||
echo "Session exported to: $output_file"
|
||||
echo "Size: $(wc -c < "$output_file") bytes"
|
||||
else
|
||||
die "Export failed"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_tap_import() {
|
||||
local import_file="$1"
|
||||
[ -z "$import_file" ] && die "Usage: rttyctl tap-import <file.json>"
|
||||
[ ! -f "$import_file" ] && die "Import file not found: $import_file"
|
||||
|
||||
echo "Importing session from: $import_file"
|
||||
|
||||
# Parse and insert into avatar-tap database
|
||||
local domain=$(jsonfilter -i "$import_file" -e '@.domain' 2>/dev/null)
|
||||
local method=$(jsonfilter -i "$import_file" -e '@.method' 2>/dev/null)
|
||||
local path=$(jsonfilter -i "$import_file" -e '@.path' 2>/dev/null)
|
||||
local label=$(jsonfilter -i "$import_file" -e '@.label' 2>/dev/null)
|
||||
|
||||
[ -z "$domain" ] || [ -z "$method" ] || [ -z "$path" ] && die "Invalid session format"
|
||||
|
||||
# Insert into database
|
||||
local now=$(date +%s)
|
||||
sqlite3 "$AVATAR_TAP_DB" "INSERT INTO sessions (domain, method, path, captured_at, label, use_count) VALUES ('$domain', '$method', '$path', $now, '$label', 0)" 2>/dev/null
|
||||
|
||||
local new_id=$(sqlite3 "$AVATAR_TAP_DB" "SELECT last_insert_rowid()" 2>/dev/null)
|
||||
|
||||
echo "Session imported with ID: $new_id"
|
||||
}
|
||||
|
||||
cmd_tap_json_sessions() {
|
||||
if [ ! -f "$AVATAR_TAP_DB" ]; then
|
||||
echo '{"sessions":[]}'
|
||||
return
|
||||
fi
|
||||
|
||||
sqlite3 -json "$AVATAR_TAP_DB" \
|
||||
"SELECT id, domain, path, method, captured_at, last_used, use_count, label FROM sessions ORDER BY captured_at DESC LIMIT 50" 2>/dev/null || echo '{"sessions":[]}'
|
||||
}
|
||||
|
||||
cmd_tap_json_session() {
|
||||
local session_id="$1"
|
||||
|
||||
if [ ! -f "$AVATAR_TAP_DB" ] || [ -z "$session_id" ]; then
|
||||
echo '{"error":"Session not found"}'
|
||||
return
|
||||
fi
|
||||
|
||||
sqlite3 -json "$AVATAR_TAP_DB" \
|
||||
"SELECT id, domain, path, method, captured_at, last_used, use_count, label FROM sessions WHERE id=$session_id" 2>/dev/null || echo '{"error":"Session not found"}'
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
@ -788,10 +1040,12 @@ RPCD Proxy:
|
||||
rpc-list <node> List available RPCD objects
|
||||
rpc-batch <node> <file.json> Execute batch RPCD calls
|
||||
|
||||
Session Management:
|
||||
sessions [node_id] List active/recent sessions
|
||||
replay <session_id> <node> Replay captured session to node
|
||||
export <session_id> Export session as JSON
|
||||
Avatar-Tap Session Replay:
|
||||
tap-sessions [domain] List captured sessions (filter by domain)
|
||||
tap-show <id> Show session details (headers, cookies)
|
||||
tap-replay <id> <node> Replay captured session to remote node
|
||||
tap-export <id> [file] Export session as JSON
|
||||
tap-import <file> Import session from JSON file
|
||||
|
||||
Server Control:
|
||||
server start Start local RTTY server
|
||||
@ -812,12 +1066,14 @@ Shared Access Tokens:
|
||||
JSON Output (for RPCD):
|
||||
json-status Status as JSON
|
||||
json-nodes Nodes as JSON
|
||||
json-tap-sessions Avatar-tap sessions as JSON
|
||||
|
||||
Examples:
|
||||
rttyctl nodes
|
||||
rttyctl rpc 10.100.0.2 luci.system-hub status
|
||||
rttyctl rpc sb-01 luci.haproxy vhost_list
|
||||
rttyctl rpc-list 192.168.255.2
|
||||
rttyctl tap-sessions photos.gk2
|
||||
rttyctl tap-replay 5 10.100.0.3
|
||||
rttyctl tap-export 12 /tmp/session.json
|
||||
|
||||
Version: $VERSION
|
||||
EOF
|
||||
@ -855,11 +1111,21 @@ case "$1" in
|
||||
sessions)
|
||||
cmd_sessions "$2"
|
||||
;;
|
||||
replay)
|
||||
cmd_replay "$2" "$3"
|
||||
# Avatar-Tap Session Replay
|
||||
tap-sessions)
|
||||
cmd_tap_sessions "$2"
|
||||
;;
|
||||
export)
|
||||
cmd_export "$2"
|
||||
tap-show)
|
||||
cmd_tap_show "$2"
|
||||
;;
|
||||
tap-replay)
|
||||
cmd_tap_replay "$2" "$3"
|
||||
;;
|
||||
tap-export)
|
||||
cmd_tap_export "$2" "$3"
|
||||
;;
|
||||
tap-import)
|
||||
cmd_tap_import "$2"
|
||||
;;
|
||||
server)
|
||||
cmd_server "$2"
|
||||
@ -882,6 +1148,12 @@ case "$1" in
|
||||
json-tokens)
|
||||
cmd_token_json_list
|
||||
;;
|
||||
json-tap-sessions)
|
||||
cmd_tap_json_sessions
|
||||
;;
|
||||
json-tap-session)
|
||||
cmd_tap_json_session "$2"
|
||||
;;
|
||||
token)
|
||||
case "$2" in
|
||||
generate)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user