secubox-openwrt/package/secubox/secubox-app-rtty-remote/files/usr/sbin/rttyctl
CyberMind-FR ac7912e0a1 feat(rtty): Add remote package installation for mesh nodes
Add rttyctl commands for remote package deployment:
- rttyctl install <node|all> <app_id> - Install package on node(s)
- rttyctl install-status <node> [app] - Check package status
- rttyctl deploy-ttyd <node|all> - Deploy ttyd web terminal

RPCD methods added:
- install_remote, install_mesh, deploy_ttyd, install_status

Features:
- Node discovery from master-link, WireGuard, P2P mesh
- Auto-enables and starts ttyd after installation
- Batch install with summary stats (installed/skipped/failed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-15 15:10:32 +01:00

1439 lines
43 KiB
Bash

#!/bin/sh
#
# rttyctl - SecuBox RTTY Remote Control CLI
#
# Remote control assistant for SecuBox mesh nodes
# Provides RPCD proxy, terminal access, and session replay
#
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
SCRIPT_NAME="rttyctl"
VERSION="0.1.0"
# Configuration
CONFIG_FILE="/etc/config/rtty-remote"
SESSION_DB="/srv/rtty-remote/sessions.db"
CACHE_DIR="/tmp/rtty-remote"
TOKEN_DIR="/tmp/rtty-remote/tokens"
LOG_FILE="/var/log/rtty-remote.log"
# Load libraries
[ -f /usr/lib/secubox/rtty-proxy.sh ] && . /usr/lib/secubox/rtty-proxy.sh
[ -f /usr/lib/secubox/rtty-session.sh ] && . /usr/lib/secubox/rtty-session.sh
[ -f /usr/lib/secubox/rtty-auth.sh ] && . /usr/lib/secubox/rtty-auth.sh
# Load master-link if available
[ -f /usr/lib/secubox/master-link.sh ] && . /usr/lib/secubox/master-link.sh
#------------------------------------------------------------------------------
# Utilities
#------------------------------------------------------------------------------
log() {
local level="$1"
shift
echo "[$level] $*" >> "$LOG_FILE"
[ "$level" = "error" ] && echo "Error: $*" >&2
}
die() {
echo "Error: $*" >&2
exit 1
}
ensure_dirs() {
mkdir -p "$CACHE_DIR" 2>/dev/null
mkdir -p "$TOKEN_DIR" 2>/dev/null
mkdir -p "$(dirname "$SESSION_DB")" 2>/dev/null
}
#------------------------------------------------------------------------------
# Token-Based Shared Access
#------------------------------------------------------------------------------
generate_token_code() {
# Generate 6-character alphanumeric code
# Use hexdump if available, fallback to awk-based pseudo-random
local chars="ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
local code=""
if command -v hexdump >/dev/null 2>&1; then
# Use hexdump for true randomness
local hex=$(head -c 6 /dev/urandom | hexdump -e '6/1 "%02x"')
local i=0
while [ $i -lt 6 ]; do
local byte=$(printf "%d" "0x$(echo "$hex" | cut -c$((i*2+1))-$((i*2+2)))")
local idx=$((byte % 32))
code="${code}$(echo "$chars" | cut -c$((idx + 1)))"
i=$((i + 1))
done
else
# Fallback: use awk with pseudo-random seed from /proc/sys/kernel/random/uuid
local seed=$(cat /proc/sys/kernel/random/uuid 2>/dev/null | tr -d '-' | cut -c1-8)
seed=$(printf "%d" "0x$seed" 2>/dev/null || echo $(($(date +%s) % 65536)))
code=$(awk -v seed="$seed" -v chars="$chars" 'BEGIN {
srand(seed)
for (i=1; i<=6; i++) {
idx = int(rand() * 32) + 1
printf "%s", substr(chars, idx, 1)
}
print ""
}')
fi
echo "$code"
}
cmd_token_generate() {
local ttl="${1:-3600}" # Default 1 hour
local permissions="${2:-rpc,terminal}"
local code=$(generate_token_code)
local now=$(date +%s)
local expires=$((now + ttl))
local node_name=$(uci -q get system.@system[0].hostname || echo "secubox")
local node_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1")
# Store token
cat > "$TOKEN_DIR/$code" << EOF
{
"code": "$code",
"created": $now,
"expires": $expires,
"ttl": $ttl,
"node_id": "$node_name",
"node_ip": "$node_ip",
"permissions": "$permissions",
"used": 0
}
EOF
log "info" "Generated token $code (expires in ${ttl}s)"
echo "Support Access Token Generated"
echo "=============================="
echo ""
echo " Code: $code"
echo " Expires: $(date -d "@$expires" "+%Y-%m-%d %H:%M:%S" 2>/dev/null || date -r $expires "+%Y-%m-%d %H:%M:%S")"
echo " TTL: ${ttl}s"
echo " Access: $permissions"
echo ""
echo "Share this code with the support person."
}
cmd_token_validate() {
local code="$1"
[ -z "$code" ] && { echo '{"valid":false,"error":"Missing code"}'; return 1; }
# Normalize code (uppercase, remove spaces)
code=$(echo "$code" | tr 'a-z' 'A-Z' | tr -d ' -')
local token_file="$TOKEN_DIR/$code"
if [ ! -f "$token_file" ]; then
echo '{"valid":false,"error":"Invalid code"}'
return 1
fi
local now=$(date +%s)
local expires=$(jsonfilter -i "$token_file" -e '@.expires')
if [ "$now" -gt "$expires" ]; then
rm -f "$token_file"
echo '{"valid":false,"error":"Token expired"}'
return 1
fi
# Mark as used
local used=$(jsonfilter -i "$token_file" -e '@.used')
used=$((used + 1))
local tmp_file="${token_file}.tmp"
jsonfilter -i "$token_file" -e '@' | sed "s/\"used\": [0-9]*/\"used\": $used/" > "$tmp_file"
mv "$tmp_file" "$token_file"
# Return token info
cat "$token_file"
}
cmd_token_list() {
echo "Active Support Tokens"
echo "====================="
echo ""
local now=$(date +%s)
local count=0
for token_file in "$TOKEN_DIR"/*; do
[ -f "$token_file" ] || continue
local code=$(basename "$token_file")
local expires=$(jsonfilter -i "$token_file" -e '@.expires' 2>/dev/null)
local created=$(jsonfilter -i "$token_file" -e '@.created' 2>/dev/null)
local used=$(jsonfilter -i "$token_file" -e '@.used' 2>/dev/null)
local perms=$(jsonfilter -i "$token_file" -e '@.permissions' 2>/dev/null)
[ -z "$expires" ] && continue
if [ "$now" -gt "$expires" ]; then
rm -f "$token_file"
continue
fi
local remaining=$((expires - now))
local mins=$((remaining / 60))
printf " %-8s %3dm left used:%d [%s]\n" "$code" "$mins" "$used" "$perms"
count=$((count + 1))
done
[ $count -eq 0 ] && echo " No active tokens"
echo ""
}
cmd_token_revoke() {
local code="$1"
[ -z "$code" ] && die "Usage: rttyctl token revoke <code>"
code=$(echo "$code" | tr 'a-z' 'A-Z' | tr -d ' -')
if [ -f "$TOKEN_DIR/$code" ]; then
rm -f "$TOKEN_DIR/$code"
echo "Token $code revoked"
log "info" "Token $code revoked"
else
echo "Token $code not found"
fi
}
cmd_token_json_list() {
local now=$(date +%s)
printf '{"tokens":['
local first=1
for token_file in "$TOKEN_DIR"/*; do
[ -f "$token_file" ] || continue
local expires=$(jsonfilter -i "$token_file" -e '@.expires' 2>/dev/null)
[ -z "$expires" ] && continue
if [ "$now" -gt "$expires" ]; then
rm -f "$token_file"
continue
fi
[ $first -eq 0 ] && printf ','
cat "$token_file"
first=0
done
printf ']}'
}
# Token-authenticated RPC call (for support access)
cmd_token_rpc() {
local code="$1"
local object="$2"
local method="$3"
shift 3
local params="$*"
[ -z "$code" ] || [ -z "$object" ] || [ -z "$method" ] && {
echo "Usage: rttyctl token-rpc <code> <object> <method> [params]"
return 1
}
# Validate token
local validation=$(cmd_token_validate "$code")
local valid=$(echo "$validation" | jsonfilter -e '@.valid' 2>/dev/null)
if [ "$valid" = "false" ]; then
echo "Error: $(echo "$validation" | jsonfilter -e '@.error')"
return 1
fi
# Check permissions
local perms=$(echo "$validation" | jsonfilter -e '@.permissions' 2>/dev/null)
case "$perms" in
*rpc*) ;;
*) echo "Error: Token does not have RPC permission"; return 1 ;;
esac
# Execute RPC locally (token grants local access)
local result=$(ubus call "$object" "$method" "${params:-{}}" 2>&1)
local rc=$?
if [ $rc -eq 0 ]; then
echo "$result"
else
echo "Error: $result"
return 1
fi
}
get_config() {
local section="$1"
local option="$2"
local default="$3"
config_load rtty-remote
config_get value "$section" "$option" "$default"
echo "$value"
}
#------------------------------------------------------------------------------
# Node Management
#------------------------------------------------------------------------------
cmd_nodes() {
echo "SecuBox RTTY Remote - Mesh Nodes"
echo "================================"
echo ""
# Try to get peers from master-link first
if command -v ml_peer_list >/dev/null 2>&1; then
local peers=$(ml_peer_list 2>/dev/null)
if [ -n "$peers" ]; then
echo "From Master-Link:"
echo "$peers" | jsonfilter -e '@.peers[*]' 2>/dev/null | while read peer; do
local fp=$(echo "$peer" | jsonfilter -e '@.fingerprint')
local name=$(echo "$peer" | jsonfilter -e '@.name')
local status=$(echo "$peer" | jsonfilter -e '@.status')
local addr=$(echo "$peer" | jsonfilter -e '@.address')
printf " %-12s %-20s %-15s %s\n" "$fp" "$name" "$addr" "$status"
done
echo ""
fi
fi
# Try secubox-p2p peers
if [ -x /usr/sbin/secubox-p2p ]; then
echo "From P2P Mesh:"
/usr/sbin/secubox-p2p peers 2>/dev/null | head -20
fi
# Also check WireGuard peers
if command -v wg >/dev/null 2>&1; then
echo ""
echo "WireGuard Peers:"
wg show all peers 2>/dev/null | while read iface peer; do
local endpoint=$(wg show "$iface" endpoints 2>/dev/null | grep "$peer" | awk '{print $2}')
local handshake=$(wg show "$iface" latest-handshakes 2>/dev/null | grep "$peer" | awk '{print $2}')
[ -n "$endpoint" ] && printf " %-15s %s (handshake: %ss ago)\n" "$endpoint" "${peer:0:12}..." "$handshake"
done
fi
}
cmd_node() {
local node_id="$1"
[ -z "$node_id" ] && die "Usage: rttyctl node <node_id>"
echo "Node: $node_id"
echo "=============="
# Check if in master-link
if command -v ml_peer_list >/dev/null 2>&1; then
local peer_info=$(ml_peer_list 2>/dev/null | jsonfilter -e "@.peers[?(@.fingerprint=='$node_id')]" 2>/dev/null)
if [ -n "$peer_info" ]; then
echo "Master-Link Status:"
echo " Name: $(echo "$peer_info" | jsonfilter -e '@.name')"
echo " Status: $(echo "$peer_info" | jsonfilter -e '@.status')"
echo " Address: $(echo "$peer_info" | jsonfilter -e '@.address')"
echo " Role: $(echo "$peer_info" | jsonfilter -e '@.role')"
fi
fi
# Try to ping
local addr=$(get_node_address "$node_id")
if [ -n "$addr" ]; then
echo ""
echo "Connectivity:"
if ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then
echo " Ping: OK"
else
echo " Ping: FAILED"
fi
fi
}
get_node_address() {
local node_id="$1"
# Check UCI for known nodes
config_load rtty-remote
local addr=""
config_get addr "node_${node_id}" address ""
[ -n "$addr" ] && { echo "$addr"; return; }
# Check master-link
if command -v ml_peer_list >/dev/null 2>&1; then
addr=$(ml_peer_list 2>/dev/null | jsonfilter -e "@.peers[?(@.fingerprint=='$node_id')].address" 2>/dev/null)
[ -n "$addr" ] && { echo "$addr"; return; }
fi
# Fallback: assume node_id is an IP
echo "$node_id"
}
#------------------------------------------------------------------------------
# RPCD Proxy - Core Feature
#------------------------------------------------------------------------------
cmd_rpc() {
local node_id="$1"
local object="$2"
local method="$3"
shift 3
local params="$*"
[ -z "$node_id" ] || [ -z "$object" ] || [ -z "$method" ] && {
echo "Usage: rttyctl rpc <node_id> <object> <method> [params_json]"
echo ""
echo "Examples:"
echo " rttyctl rpc 10.100.0.2 luci.system-hub status"
echo " rttyctl rpc sb-01 luci.haproxy vhost_list"
echo " rttyctl rpc 192.168.255.2 system board"
exit 1
}
local addr=$(get_node_address "$node_id")
[ -z "$addr" ] && die "Cannot resolve node address for: $node_id"
if [ -z "$params" ] || [ "$params" = "{}" ]; then
params="{}"
fi
log "info" "RPC call to $addr: $object.$method"
# Check if local address - use direct ubus for full access
local is_local=0
case "$addr" in
127.0.0.1|localhost|192.168.255.1|$(uci -q get network.lan.ipaddr))
is_local=1
;;
esac
local result=""
if [ "$is_local" = "1" ]; then
# Use direct ubus call for local node (full access)
result=$(ubus call "$object" "$method" "$params" 2>&1)
local ubus_rc=$?
if [ $ubus_rc -ne 0 ]; then
die "ubus error: $result"
fi
else
# Remote node - use HTTP JSON-RPC
local auth_token=$(get_auth_token "$addr")
local rpc_id=$(date +%s%N | cut -c1-13)
local request=$(cat << EOF
{"jsonrpc":"2.0","id":$rpc_id,"method":"call","params":["$auth_token","$object","$method",$params]}
EOF
)
local timeout=$(get_config proxy rpc_timeout 30)
local http_port=$(get_config proxy http_port 8081)
local url="http://${addr}:${http_port}/ubus"
local response=$(curl -s -m "$timeout" \
-H "Content-Type: application/json" \
-d "$request" \
"$url" 2>&1)
local curl_rc=$?
if [ $curl_rc -ne 0 ]; then
die "Connection failed to $addr (curl error: $curl_rc)"
fi
# Check for JSON-RPC error
local error=$(echo "$response" | jsonfilter -e '@.error.message' 2>/dev/null)
if [ -n "$error" ]; then
die "RPC error: $error"
fi
# Extract result
result=$(echo "$response" | jsonfilter -e '@.result[1]' 2>/dev/null)
fi
if [ -z "$result" ]; then
result=$(echo "$response" | jsonfilter -e '@.result' 2>/dev/null)
fi
echo "$result"
}
cmd_rpc_list() {
local node_id="$1"
[ -z "$node_id" ] && die "Usage: rttyctl rpc-list <node_id>"
local addr=$(get_node_address "$node_id")
[ -z "$addr" ] && die "Cannot resolve node address for: $node_id"
echo "RPCD Objects on $node_id ($addr)"
echo "================================="
# Get session first
local auth_token=$(get_auth_token "$addr")
# List ubus objects
json_init
json_add_string "jsonrpc" "2.0"
json_add_int "id" 1
json_add_string "method" "list"
json_add_array "params"
json_add_string "" "$auth_token"
json_add_string "" "*"
json_close_array
local request=$(json_dump)
local http_port=$(get_config proxy http_port 8081)
local response=$(curl -s -m 30 \
-H "Content-Type: application/json" \
-d "$request" \
"http://${addr}:${http_port}/ubus" 2>&1)
# Parse and display objects
echo "$response" | jsonfilter -e '@.result[1]' 2>/dev/null | \
python3 -c "import sys,json; d=json.load(sys.stdin); [print(f' {k}') for k in sorted(d.keys())]" 2>/dev/null || \
echo "$response"
}
cmd_rpc_batch() {
local node_id="$1"
local batch_file="$2"
[ -z "$node_id" ] || [ -z "$batch_file" ] && die "Usage: rttyctl rpc-batch <node_id> <file.json>"
[ ! -f "$batch_file" ] && die "Batch file not found: $batch_file"
echo "Executing batch RPC to $node_id..."
local count=0
while read -r line; do
[ -z "$line" ] && continue
local object=$(echo "$line" | jsonfilter -e '@.object')
local method=$(echo "$line" | jsonfilter -e '@.method')
local params=$(echo "$line" | jsonfilter -e '@.params')
echo "[$count] $object.$method"
cmd_rpc "$node_id" "$object" "$method" "$params"
echo ""
count=$((count + 1))
done < "$batch_file"
echo "Executed $count calls"
}
#------------------------------------------------------------------------------
# Authentication
#------------------------------------------------------------------------------
get_auth_token() {
local addr="$1"
local cache_file="$CACHE_DIR/auth_${addr//[.:]/_}"
# Check cache
if [ -f "$cache_file" ]; then
local cached=$(cat "$cache_file")
local ts=$(echo "$cached" | cut -d: -f1)
local token=$(echo "$cached" | cut -d: -f2-)
local now=$(date +%s)
local ttl=$(get_config main session_ttl 3600)
if [ $((now - ts)) -lt $ttl ]; then
echo "$token"
return 0
fi
fi
# Get new session via login
# For now, use anonymous session (00000000000000000000000000000000)
# TODO: Implement proper master-link authentication
local anon_token="00000000000000000000000000000000"
# Try to get authenticated session
json_init
json_add_string "jsonrpc" "2.0"
json_add_int "id" 1
json_add_string "method" "call"
json_add_array "params"
json_add_string "" "$anon_token"
json_add_string "" "session"
json_add_string "" "login"
json_add_object ""
json_add_string "username" "root"
json_add_string "password" ""
json_close_object
json_close_array
local request=$(json_dump)
local http_port=$(get_config proxy http_port 8081)
local response=$(curl -s -m 10 \
-H "Content-Type: application/json" \
-d "$request" \
"http://${addr}:${http_port}/ubus" 2>&1)
local token=$(echo "$response" | jsonfilter -e '@.result[1].ubus_rpc_session' 2>/dev/null)
if [ -n "$token" ] && [ "$token" != "null" ]; then
echo "$(date +%s):$token" > "$cache_file"
echo "$token"
else
# Fallback to anonymous
echo "$anon_token"
fi
}
cmd_auth() {
local node_id="$1"
[ -z "$node_id" ] && die "Usage: rttyctl auth <node_id>"
local addr=$(get_node_address "$node_id")
echo "Authenticating to $node_id ($addr)..."
# Clear cache to force re-auth
rm -f "$CACHE_DIR/auth_${addr//[.:]/_}" 2>/dev/null
local token=$(get_auth_token "$addr")
if [ -n "$token" ] && [ "$token" != "00000000000000000000000000000000" ]; then
echo "Authenticated successfully"
echo "Session: ${token:0:16}..."
else
echo "Using anonymous session (limited access)"
fi
}
cmd_revoke() {
local node_id="$1"
[ -z "$node_id" ] && die "Usage: rttyctl revoke <node_id>"
local addr=$(get_node_address "$node_id")
rm -f "$CACHE_DIR/auth_${addr//[.:]/_}" 2>/dev/null
echo "Revoked authentication for $node_id"
}
#------------------------------------------------------------------------------
# Terminal/Connection (Placeholder for RTTY)
#------------------------------------------------------------------------------
cmd_connect() {
local node_id="$1"
[ -z "$node_id" ] && die "Usage: rttyctl connect <node_id>"
local addr=$(get_node_address "$node_id")
echo "Connecting to $node_id ($addr)..."
echo ""
echo "Note: Full terminal support requires RTTY package."
echo "For now, use SSH:"
echo " ssh root@$addr"
echo ""
echo "Or use RPCD proxy:"
echo " rttyctl rpc $node_id system board"
}
cmd_disconnect() {
local session_id="$1"
[ -z "$session_id" ] && die "Usage: rttyctl disconnect <session_id>"
echo "Session $session_id disconnected"
}
#------------------------------------------------------------------------------
# Session Management (RTTY Remote Sessions)
#------------------------------------------------------------------------------
cmd_sessions() {
local node_id="$1"
echo "Active Sessions"
echo "==============="
if [ ! -f "$SESSION_DB" ]; then
echo "No sessions recorded"
return
fi
sqlite3 "$SESSION_DB" "SELECT id, node_id, type, started_at FROM sessions ORDER BY started_at DESC LIMIT 20" 2>/dev/null || \
echo "Session database not initialized"
}
#------------------------------------------------------------------------------
# Avatar-Tap Integration - Session Capture & Replay
#------------------------------------------------------------------------------
# Load avatar-tap database path from UCI config
get_avatar_tap_db() {
config_load avatar-tap 2>/dev/null
local db_path
config_get db_path main db_path '/srv/avatar-tap/sessions.db'
echo "$db_path"
}
AVATAR_TAP_DB=$(get_avatar_tap_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" ] && {
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
}
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_tap_export() {
local session_id="$1"
local output_file="$2"
[ -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"}'
}
#------------------------------------------------------------------------------
# Remote Package Installation
#------------------------------------------------------------------------------
cmd_install() {
local target="$1"
local app_id="$2"
[ -z "$target" ] || [ -z "$app_id" ] && {
echo "Usage: rttyctl install <node|all> <app_id>"
echo ""
echo "Install a package on remote mesh node(s)."
echo ""
echo "Examples:"
echo " rttyctl install 192.168.255.2 ttyd"
echo " rttyctl install sb-office ttyd"
echo " rttyctl install all ttyd # Install on ALL mesh nodes"
echo ""
echo "Common apps: ttyd, crowdsec, netifyd, haproxy"
exit 1
}
if [ "$target" = "all" ]; then
cmd_install_all "$app_id"
return $?
fi
local addr=$(get_node_address "$target")
[ -z "$addr" ] && die "Cannot resolve node address for: $target"
echo "Installing '$app_id' on $target ($addr)..."
echo ""
# Check if node is reachable
if ! ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then
die "Node $target ($addr) is not reachable"
fi
# Check if already installed
echo "Checking if already installed..."
local check_result=$(cmd_rpc "$target" "luci.secubox" "check_package" "{\"package\":\"$app_id\"}" 2>/dev/null)
local installed=$(echo "$check_result" | jsonfilter -e '@.installed' 2>/dev/null)
if [ "$installed" = "true" ] || [ "$installed" = "1" ]; then
echo "Package '$app_id' is already installed on $target"
return 0
fi
# Install via remote RPC
echo "Installing package..."
local install_result=$(cmd_rpc "$target" "luci.secubox" "install_appstore_app" "{\"app_id\":\"$app_id\"}" 2>&1)
local rc=$?
if [ $rc -eq 0 ]; then
local success=$(echo "$install_result" | jsonfilter -e '@.success' 2>/dev/null)
if [ "$success" = "true" ] || [ "$success" = "1" ]; then
echo "✓ Successfully installed '$app_id' on $target"
log "info" "Installed $app_id on $target ($addr)"
# If ttyd, enable and start the service
if [ "$app_id" = "ttyd" ]; then
echo ""
echo "Enabling and starting ttyd service..."
cmd_rpc "$target" "luci" "setInitAction" '{"name":"ttyd","action":"enable"}' >/dev/null 2>&1
cmd_rpc "$target" "luci" "setInitAction" '{"name":"ttyd","action":"start"}' >/dev/null 2>&1
echo "✓ ttyd service enabled and started"
echo ""
echo "Web terminal available at: http://$addr:7681/"
fi
else
local error=$(echo "$install_result" | jsonfilter -e '@.error' 2>/dev/null)
echo "✗ Installation failed: ${error:-Unknown error}"
echo "Raw response: $install_result"
return 1
fi
else
echo "✗ RPC call failed"
echo "Response: $install_result"
return 1
fi
}
cmd_install_all() {
local app_id="$1"
[ -z "$app_id" ] && die "Usage: rttyctl install all <app_id>"
echo "Installing '$app_id' on ALL mesh nodes..."
echo "========================================="
echo ""
local success_count=0
local fail_count=0
local skip_count=0
# Get all nodes from various sources
local nodes=""
# From master-link
if command -v ml_peer_list >/dev/null 2>&1; then
local ml_nodes=$(ml_peer_list 2>/dev/null | jsonfilter -e '@.peers[*].address' 2>/dev/null)
nodes="$nodes $ml_nodes"
fi
# From WireGuard peers
if command -v wg >/dev/null 2>&1; then
local wg_nodes=$(wg show all endpoints 2>/dev/null | awk '{print $2}' | cut -d: -f1)
nodes="$nodes $wg_nodes"
fi
# From P2P mesh
if [ -x /usr/sbin/secubox-p2p ]; then
local p2p_nodes=$(/usr/sbin/secubox-p2p peers --json 2>/dev/null | jsonfilter -e '@[*].address' 2>/dev/null)
nodes="$nodes $p2p_nodes"
fi
# Deduplicate and process
local unique_nodes=$(echo "$nodes" | tr ' ' '\n' | sort -u | grep -v '^$')
local total=$(echo "$unique_nodes" | wc -l)
echo "Found $total mesh nodes"
echo ""
for node in $unique_nodes; do
[ -z "$node" ] && continue
echo "[$node]"
# Skip localhost
case "$node" in
127.0.0.1|localhost|$(uci -q get network.lan.ipaddr))
echo " Skipping (local node)"
skip_count=$((skip_count + 1))
continue
;;
esac
# Check reachability
if ! ping -c 1 -W 2 "$node" >/dev/null 2>&1; then
echo " ✗ Not reachable"
fail_count=$((fail_count + 1))
continue
fi
# Try to install
local result=$(cmd_rpc "$node" "luci.secubox" "install_appstore_app" "{\"app_id\":\"$app_id\"}" 2>&1)
local success=$(echo "$result" | jsonfilter -e '@.success' 2>/dev/null)
if [ "$success" = "true" ] || [ "$success" = "1" ]; then
echo " ✓ Installed"
success_count=$((success_count + 1))
# Enable ttyd if that's what we installed
if [ "$app_id" = "ttyd" ]; then
cmd_rpc "$node" "luci" "setInitAction" '{"name":"ttyd","action":"enable"}' >/dev/null 2>&1
cmd_rpc "$node" "luci" "setInitAction" '{"name":"ttyd","action":"start"}' >/dev/null 2>&1
echo " ✓ ttyd enabled and started"
fi
else
local error=$(echo "$result" | jsonfilter -e '@.error' 2>/dev/null)
if echo "$error" | grep -qi "already installed"; then
echo " ⊘ Already installed"
skip_count=$((skip_count + 1))
else
echo " ✗ Failed: ${error:-Unknown error}"
fail_count=$((fail_count + 1))
fi
fi
done
echo ""
echo "Summary: $success_count installed, $skip_count skipped, $fail_count failed"
log "info" "Mesh install of $app_id: $success_count ok, $skip_count skip, $fail_count fail"
}
cmd_install_status() {
local target="$1"
local app_id="$2"
[ -z "$target" ] && {
echo "Usage: rttyctl install-status <node> [app_id]"
exit 1
}
local addr=$(get_node_address "$target")
[ -z "$addr" ] && die "Cannot resolve node address for: $target"
echo "Package Status on $target ($addr)"
echo "=================================="
if [ -n "$app_id" ]; then
# Check specific package
local result=$(cmd_rpc "$target" "luci.secubox" "check_package" "{\"package\":\"$app_id\"}" 2>/dev/null)
local installed=$(echo "$result" | jsonfilter -e '@.installed' 2>/dev/null)
local version=$(echo "$result" | jsonfilter -e '@.version' 2>/dev/null)
if [ "$installed" = "true" ] || [ "$installed" = "1" ]; then
echo " $app_id: installed (v${version:-?})"
else
echo " $app_id: not installed"
fi
else
# List all SecuBox packages
local result=$(cmd_rpc "$target" "luci.secubox" "list_installed" '{}' 2>/dev/null)
echo "$result" | jsonfilter -e '@.packages[*]' 2>/dev/null | while read pkg; do
local name=$(echo "$pkg" | jsonfilter -e '@.name')
local ver=$(echo "$pkg" | jsonfilter -e '@.version')
echo " $name: $ver"
done
fi
}
cmd_deploy_ttyd() {
local target="$1"
[ -z "$target" ] && {
echo "Usage: rttyctl deploy-ttyd <node|all>"
echo ""
echo "Deploy ttyd web terminal to mesh node(s)."
echo "This is a shortcut for: rttyctl install <target> ttyd"
exit 1
}
echo "Deploying ttyd Web Terminal"
echo "==========================="
echo ""
cmd_install "$target" "ttyd"
}
#------------------------------------------------------------------------------
# Server Management
#------------------------------------------------------------------------------
cmd_server() {
local action="$1"
case "$action" in
start)
/etc/init.d/rtty-remote start
echo "RTTY Remote server started"
;;
stop)
/etc/init.d/rtty-remote stop
echo "RTTY Remote server stopped"
;;
status)
echo "RTTY Remote Server Status"
echo "========================="
local enabled=$(get_config main enabled 0)
local port=$(get_config main server_port 7681)
echo "Enabled: $enabled"
echo "Port: $port"
if pgrep -f "rttyctl server-daemon" >/dev/null 2>&1; then
echo "Running: yes"
else
echo "Running: no"
fi
;;
*)
echo "Usage: rttyctl server start|stop|status"
;;
esac
}
cmd_server_daemon() {
# Internal: daemon mode for procd
ensure_dirs
log "info" "RTTY Remote daemon starting..."
# TODO: Implement HTTP server for incoming RPC proxy requests
# For now, just keep the process alive
while true; do
sleep 60
done
}
#------------------------------------------------------------------------------
# JSON Output (for RPCD)
#------------------------------------------------------------------------------
cmd_json_status() {
json_init
json_add_boolean "enabled" "$(get_config main enabled 0)"
json_add_int "port" "$(get_config main server_port 7681)"
if pgrep -f "rttyctl server-daemon" >/dev/null 2>&1; then
json_add_boolean "running" 1
else
json_add_boolean "running" 0
fi
json_add_int "active_sessions" 0
json_add_int "total_nodes" 0
json_dump
}
cmd_json_nodes() {
json_init
json_add_array "nodes"
# Add nodes from master-link
if command -v ml_peer_list >/dev/null 2>&1; then
ml_peer_list 2>/dev/null | jsonfilter -e '@.peers[*]' 2>/dev/null | while read peer; do
json_add_object ""
json_add_string "id" "$(echo "$peer" | jsonfilter -e '@.fingerprint')"
json_add_string "name" "$(echo "$peer" | jsonfilter -e '@.name')"
json_add_string "address" "$(echo "$peer" | jsonfilter -e '@.address')"
json_add_string "status" "$(echo "$peer" | jsonfilter -e '@.status')"
json_close_object
done
fi
json_close_array
json_dump
}
#------------------------------------------------------------------------------
# Help
#------------------------------------------------------------------------------
show_help() {
cat << 'EOF'
rttyctl - SecuBox RTTY Remote Control
Usage: rttyctl <command> [options]
Node Management:
nodes List all mesh nodes with status
node <id> Show detailed node info
connect <node_id> Start terminal session to node
disconnect <session_id> End terminal session
RPCD Proxy:
rpc <node> <object> <method> [params] Execute remote RPCD call
rpc-list <node> List available RPCD objects
rpc-batch <node> <file.json> Execute batch RPCD calls
Remote Package Installation:
install <node|all> <app_id> Install package on node(s)
install-status <node> [app] Check package installation status
deploy-ttyd <node|all> Deploy ttyd web terminal (shortcut)
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
server stop Stop local RTTY server
server status Show server status
Authentication:
auth <node_id> Authenticate to remote node
revoke <node_id> Revoke authentication
Shared Access Tokens:
token generate [ttl] [perms] Generate support token (default: 3600s, rpc,terminal)
token list List active tokens
token validate <code> Validate a token
token revoke <code> Revoke a token
token-rpc <code> <obj> <method> [params] Execute RPC with token auth
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 tap-sessions photos.gk2
rttyctl tap-replay 5 10.100.0.3
rttyctl tap-export 12 /tmp/session.json
Version: $VERSION
EOF
}
#------------------------------------------------------------------------------
# Main
#------------------------------------------------------------------------------
ensure_dirs
case "$1" in
nodes)
cmd_nodes
;;
node)
cmd_node "$2"
;;
connect)
cmd_connect "$2"
;;
disconnect)
cmd_disconnect "$2"
;;
rpc)
shift
cmd_rpc "$@"
;;
rpc-list)
cmd_rpc_list "$2"
;;
rpc-batch)
cmd_rpc_batch "$2" "$3"
;;
install)
cmd_install "$2" "$3"
;;
install-status)
cmd_install_status "$2" "$3"
;;
deploy-ttyd)
cmd_deploy_ttyd "$2"
;;
sessions)
cmd_sessions "$2"
;;
# Avatar-Tap Session Replay
tap-sessions)
cmd_tap_sessions "$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"
;;
server-daemon)
cmd_server_daemon
;;
auth)
cmd_auth "$2"
;;
revoke)
cmd_revoke "$2"
;;
json-status)
cmd_json_status
;;
json-nodes)
cmd_json_nodes
;;
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)
cmd_token_generate "$3" "$4"
;;
list)
cmd_token_list
;;
validate)
cmd_token_validate "$3"
;;
revoke)
cmd_token_revoke "$3"
;;
*)
echo "Usage: rttyctl token generate|list|validate|revoke"
;;
esac
;;
token-rpc)
shift
cmd_token_rpc "$@"
;;
-h|--help|help)
show_help
;;
*)
show_help
exit 1
;;
esac