From fe762b6eb1f4e1d5f5d7eafcb005db2073d6310b Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 9 Mar 2026 13:28:06 +0100 Subject: [PATCH] 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 --- .claude/settings.local.json | 12 +- .../root/usr/libexec/rpcd/luci.rtty-remote | 126 +++++++ .../luci-static/resources/system-hub/api.js | 10 + .../resources/view/system-hub/services.js | 169 +++++++++- .../root/usr/libexec/rpcd/luci.system-hub | 34 ++ .../share/rpcd/acl.d/luci-app-system-hub.json | 1 + .../files/usr/sbin/rttyctl | 310 ++++++++++++++++-- 7 files changed, 640 insertions(+), 22 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6caf4a7a..77dc8975 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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:*)" ] } } diff --git a/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote b/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote index 13c7fc21..c993c087 100644 --- a/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote +++ b/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote @@ -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 #------------------------------------------------------------------------------ diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js index 5702add5..0a46f492 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/system-hub/api.js @@ -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, diff --git a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js index 83ce6e04..0dd27b3a 100644 --- a/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js +++ b/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js @@ -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); }, diff --git a/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub b/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub index cb87d243..08e34381 100755 --- a/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub +++ b/package/secubox/luci-app-system-hub/root/usr/libexec/rpcd/luci.system-hub @@ -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 ;; diff --git a/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json b/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json index f50a1d06..99ca89d9 100644 --- a/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json +++ b/package/secubox/luci-app-system-hub/root/usr/share/rpcd/acl.d/luci-app-system-hub.json @@ -7,6 +7,7 @@ "status", "get_system_info", "get_health", + "get_service_health", "get_components", "get_components_by_category", "list_services", diff --git a/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl b/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl index 743be9a9..7e5ba4f4 100644 --- a/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl +++ b/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl @@ -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 " + + 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 " + [ -z "$session_id" ] || [ -z "$target_node" ] && { + echo "Usage: rttyctl tap-replay " + 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 " + local output_file="$2" - echo "Exporting session $session_id..." - echo "Note: Session export requires implementation" + [ -z "$session_id" ] && die "Usage: rttyctl tap-export [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 " + [ ! -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 List available RPCD objects rpc-batch Execute batch RPCD calls -Session Management: - sessions [node_id] List active/recent sessions - replay Replay captured session to node - export Export session as JSON +Avatar-Tap Session Replay: + tap-sessions [domain] List captured sessions (filter by domain) + tap-show Show session details (headers, cookies) + tap-replay Replay captured session to remote node + tap-export [file] Export session as JSON + tap-import 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)