#!/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=$(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 [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 " 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 [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 " 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 " [ ! -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 " 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 " 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 " 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 " 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 " 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 " 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 [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"}' } #------------------------------------------------------------------------------ # Remote Package Installation #------------------------------------------------------------------------------ cmd_install() { local target="$1" local app_id="$2" [ -z "$target" ] || [ -z "$app_id" ] && { echo "Usage: rttyctl install " 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 " 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 [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 " echo "" echo "Deploy ttyd web terminal to mesh node(s)." echo "This is a shortcut for: rttyctl install 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 [options] Node Management: nodes List all mesh nodes with status node Show detailed node info connect Start terminal session to node disconnect End terminal session RPCD Proxy: rpc [params] Execute remote RPCD call rpc-list List available RPCD objects rpc-batch Execute batch RPCD calls Remote Package Installation: install Install package on node(s) install-status [app] Check package installation status deploy-ttyd Deploy ttyd web terminal (shortcut) 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 server stop Stop local RTTY server server status Show server status Authentication: auth Authenticate to remote node revoke Revoke authentication Shared Access Tokens: token generate [ttl] [perms] Generate support token (default: 3600s, rpc,terminal) token list List active tokens token validate Validate a token token revoke Revoke a token token-rpc [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