secubox-openwrt/package/secubox/luci-app-rtty-remote/root/usr/libexec/rpcd/luci.rtty-remote
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

788 lines
22 KiB
Bash

#!/bin/sh
# RPCD handler for luci-app-rtty-remote
# Provides RPC interface for RTTY Remote Control
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
# 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
RTTYCTL="/usr/sbin/rttyctl"
#------------------------------------------------------------------------------
# Helper functions
#------------------------------------------------------------------------------
get_config() {
local section="$1"
local option="$2"
local default="$3"
config_load rtty-remote
config_get value "$section" "$option" "$default"
echo "$value"
}
#------------------------------------------------------------------------------
# RPC Methods
#------------------------------------------------------------------------------
# Get server status
method_status() {
json_init
config_load rtty-remote
local enabled
config_get enabled main enabled '0'
json_add_boolean "enabled" "$enabled"
local port
config_get port main server_port '7681'
json_add_int "port" "$port"
# Check if running
if pgrep -f "rttyctl server-daemon" >/dev/null 2>&1; then
json_add_boolean "running" 1
else
json_add_boolean "running" 0
fi
# Stats
if [ -f /srv/rtty-remote/sessions.db ]; then
local stats=$(session_stats 2>/dev/null)
json_add_int "total_sessions" "$(echo "$stats" | jsonfilter -e '@.total_sessions' 2>/dev/null || echo 0)"
json_add_int "active_sessions" "$(echo "$stats" | jsonfilter -e '@.active_sessions' 2>/dev/null || echo 0)"
json_add_int "total_rpc_calls" "$(echo "$stats" | jsonfilter -e '@.total_rpc_calls' 2>/dev/null || echo 0)"
json_add_int "unique_nodes" "$(echo "$stats" | jsonfilter -e '@.unique_nodes' 2>/dev/null || echo 0)"
else
json_add_int "total_sessions" 0
json_add_int "active_sessions" 0
json_add_int "total_rpc_calls" 0
json_add_int "unique_nodes" 0
fi
json_dump
}
# Get mesh nodes
method_get_nodes() {
local nodes=$($RTTYCTL json-nodes 2>/dev/null)
[ -z "$nodes" ] && nodes='{"nodes":[]}'
echo "$nodes"
}
# Get single node details
method_get_node() {
local node_id
read -r input
json_load "$input"
json_get_var node_id node_id
[ -z "$node_id" ] && {
echo '{"error":"Missing node_id"}'
return
}
# Get node info from master-link or P2P
json_init
json_add_string "node_id" "$node_id"
# Try to get address
local addr=$($RTTYCTL node "$node_id" 2>&1 | grep "Address:" | awk '{print $2}')
json_add_string "address" "$addr"
# Check connectivity
if [ -n "$addr" ] && ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then
json_add_string "status" "online"
else
json_add_string "status" "offline"
fi
json_dump
}
# Execute remote RPC call
method_rpc_call() {
local node_id object method params
read -r input
json_load "$input"
json_get_var node_id node_id
json_get_var object object
json_get_var method method
json_get_var params params
[ -z "$node_id" ] || [ -z "$object" ] || [ -z "$method" ] && {
echo '{"error":"Missing required parameters (node_id, object, method)"}'
return
}
# Execute via rttyctl
local result=$($RTTYCTL rpc "$node_id" "$object" "$method" "$params" 2>&1)
local rc=$?
if [ $rc -eq 0 ] && [ -n "$result" ]; then
# Check if result is valid JSON
if echo "$result" | jsonfilter -e '@' >/dev/null 2>&1; then
# Output success with embedded JSON result
printf '{"success":true,"result":%s}' "$result"
else
# Result is not JSON, wrap as string
printf '{"success":true,"result":"%s"}' "$(echo "$result" | sed 's/"/\\"/g')"
fi
else
# Error case
local err_msg=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
printf '{"success":false,"error":"%s"}' "$err_msg"
fi
}
# List remote RPCD objects
method_rpc_list() {
local node_id
read -r input
json_load "$input"
json_get_var node_id node_id
[ -z "$node_id" ] && {
echo '{"error":"Missing node_id"}'
return
}
local result=$($RTTYCTL rpc-list "$node_id" 2>&1)
echo "$result"
}
# Get sessions
method_get_sessions() {
local node_id limit
read -r input
json_load "$input"
json_get_var node_id node_id
json_get_var limit limit
[ -z "$limit" ] && limit=50
session_list "$node_id" "$limit"
}
# Start server
method_server_start() {
/etc/init.d/rtty-remote start 2>&1
json_init
json_add_boolean "success" 1
json_dump
}
# Stop server
method_server_stop() {
/etc/init.d/rtty-remote stop 2>&1
json_init
json_add_boolean "success" 1
json_dump
}
# Get settings
method_get_settings() {
json_init
config_load rtty-remote
json_add_object "main"
local val
config_get val main enabled '0'
json_add_boolean "enabled" "$val"
config_get val main server_port '7681'
json_add_int "server_port" "$val"
config_get val main auth_method 'master-link'
json_add_string "auth_method" "$val"
config_get val main session_ttl '3600'
json_add_int "session_ttl" "$val"
config_get val main max_sessions '10'
json_add_int "max_sessions" "$val"
json_close_object
json_add_object "security"
config_get val security require_wg '1'
json_add_boolean "require_wg" "$val"
config_get val security allowed_networks ''
json_add_string "allowed_networks" "$val"
json_close_object
json_add_object "proxy"
config_get val proxy rpc_timeout '30'
json_add_int "rpc_timeout" "$val"
config_get val proxy batch_limit '50'
json_add_int "batch_limit" "$val"
config_get val proxy cache_ttl '60'
json_add_int "cache_ttl" "$val"
json_close_object
json_dump
}
# Set settings
method_set_settings() {
read -r input
# Parse and apply settings
local enabled=$(echo "$input" | jsonfilter -e '@.main.enabled' 2>/dev/null)
[ -n "$enabled" ] && uci set rtty-remote.main.enabled="$enabled"
local port=$(echo "$input" | jsonfilter -e '@.main.server_port' 2>/dev/null)
[ -n "$port" ] && uci set rtty-remote.main.server_port="$port"
local auth=$(echo "$input" | jsonfilter -e '@.main.auth_method' 2>/dev/null)
[ -n "$auth" ] && uci set rtty-remote.main.auth_method="$auth"
local ttl=$(echo "$input" | jsonfilter -e '@.main.session_ttl' 2>/dev/null)
[ -n "$ttl" ] && uci set rtty-remote.main.session_ttl="$ttl"
uci commit rtty-remote
json_init
json_add_boolean "success" 1
json_dump
}
# Replay session
method_replay_session() {
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 '{"error":"Missing session_id or target_node"}'
return
}
local result=$($RTTYCTL replay "$session_id" "$target_node" 2>&1)
json_init
json_add_boolean "success" 1
json_add_string "message" "$result"
json_dump
}
# Connect to node (start terminal)
method_connect() {
local node_id
read -r input
json_load "$input"
json_get_var node_id node_id
[ -z "$node_id" ] && {
echo '{"error":"Missing node_id"}'
return
}
# Get node address
local addr=$($RTTYCTL node "$node_id" 2>&1 | grep "Address:" | awk '{print $2}')
json_init
json_add_string "node_id" "$node_id"
json_add_string "address" "$addr"
json_add_string "terminal_url" "ws://localhost:7681/ws"
json_add_string "ssh_command" "ssh root@$addr"
json_dump
}
#------------------------------------------------------------------------------
# Token-Based Shared Access
#------------------------------------------------------------------------------
# Generate support access token
method_token_generate() {
local ttl permissions
read -r input
json_load "$input" 2>/dev/null
json_get_var ttl ttl
json_get_var permissions permissions
[ -z "$ttl" ] && ttl=3600
[ -z "$permissions" ] && permissions="rpc,terminal"
# Generate token via rttyctl
local output=$($RTTYCTL token generate "$ttl" "$permissions" 2>&1)
# Extract code from output
local code=$(echo "$output" | grep "Code:" | awk '{print $2}')
local expires=$(echo "$output" | grep "Expires:" | sed 's/.*Expires: //')
if [ -n "$code" ]; then
json_init
json_add_boolean "success" 1
json_add_string "code" "$code"
json_add_int "ttl" "$ttl"
json_add_string "permissions" "$permissions"
json_add_string "expires" "$expires"
json_dump
else
printf '{"success":false,"error":"Failed to generate token"}'
fi
}
# List active tokens
method_token_list() {
$RTTYCTL json-tokens 2>/dev/null || echo '{"tokens":[]}'
}
# Validate token (for support access)
method_token_validate() {
local code
read -r input
json_load "$input"
json_get_var code code
[ -z "$code" ] && {
echo '{"valid":false,"error":"Missing code"}'
return
}
$RTTYCTL token validate "$code" 2>/dev/null
}
# Revoke token
method_token_revoke() {
local code
read -r input
json_load "$input"
json_get_var code code
[ -z "$code" ] && {
echo '{"success":false,"error":"Missing code"}'
return
}
$RTTYCTL token revoke "$code" >/dev/null 2>&1
json_init
json_add_boolean "success" 1
json_dump
}
# Start terminal session to remote node
method_start_terminal() {
local node_id
read -r input
json_load "$input"
json_get_var node_id node_id
[ -z "$node_id" ] && {
echo '{"success":false,"error":"Missing node_id"}'
return
}
# Get node address
local addr=$($RTTYCTL node "$node_id" 2>&1 | grep "Address:" | awk '{print $2}')
[ -z "$addr" ] && addr="$node_id"
# Check if we can reach the remote ttyd directly
local remote_port=7681
if curl -s -m 2 "http://${addr}:${remote_port}/" >/dev/null 2>&1; then
# Remote ttyd is accessible
json_init
json_add_boolean "success" 1
json_add_string "type" "direct"
json_add_string "url" "http://${addr}:${remote_port}"
json_add_string "node_id" "$node_id"
json_add_string "address" "$addr"
json_dump
else
# Remote ttyd not accessible, provide SSH info
json_init
json_add_boolean "success" 1
json_add_string "type" "ssh"
json_add_string "ssh_command" "ssh root@${addr}"
json_add_string "node_id" "$node_id"
json_add_string "address" "$addr"
json_dump
fi
}
# Execute RPC with token authentication (no LuCI session needed)
method_token_rpc() {
local code object method params
read -r input
json_load "$input"
json_get_var code code
json_get_var object object
json_get_var method method
json_get_var params params
[ -z "$code" ] || [ -z "$object" ] || [ -z "$method" ] && {
echo '{"success":false,"error":"Missing required parameters (code, object, method)"}'
return
}
[ -z "$params" ] && params="{}"
# Execute via rttyctl with token auth
local result=$($RTTYCTL token-rpc "$code" "$object" "$method" "$params" 2>&1)
local rc=$?
if [ $rc -eq 0 ] && [ -n "$result" ]; then
if echo "$result" | jsonfilter -e '@' >/dev/null 2>&1; then
printf '{"success":true,"result":%s}' "$result"
else
printf '{"success":true,"result":"%s"}' "$(echo "$result" | sed 's/"/\\"/g')"
fi
else
local err_msg=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
printf '{"success":false,"error":"%s"}' "$err_msg"
fi
}
#------------------------------------------------------------------------------
# Remote Package Installation
#------------------------------------------------------------------------------
# Install package on remote node
method_install_remote() {
local node_id app_id
read -r input
json_load "$input"
json_get_var node_id node_id
json_get_var app_id app_id
[ -z "$node_id" ] || [ -z "$app_id" ] && {
echo '{"success":false,"error":"Missing node_id or app_id"}'
return
}
# Execute via rttyctl
local result=$($RTTYCTL install "$node_id" "$app_id" 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ] && echo "$result" | grep -q "✓"; then
json_add_boolean "success" 1
json_add_string "message" "Package $app_id installed on $node_id"
json_add_string "output" "$result"
else
json_add_boolean "success" 0
json_add_string "error" "$result"
fi
json_dump
}
# Install package on all mesh nodes
method_install_mesh() {
local app_id
read -r input
json_load "$input"
json_get_var app_id app_id
[ -z "$app_id" ] && {
echo '{"success":false,"error":"Missing app_id"}'
return
}
# Execute via rttyctl with 'all' target
local result=$($RTTYCTL install all "$app_id" 2>&1)
local rc=$?
# Parse summary line
local installed=$(echo "$result" | grep "Summary:" | sed 's/.*: //' | cut -d, -f1 | awk '{print $1}')
local skipped=$(echo "$result" | grep "Summary:" | sed 's/.*: //' | cut -d, -f2 | awk '{print $1}')
local failed=$(echo "$result" | grep "Summary:" | sed 's/.*: //' | cut -d, -f3 | awk '{print $1}')
json_init
json_add_boolean "success" 1
json_add_string "app_id" "$app_id"
json_add_int "installed" "${installed:-0}"
json_add_int "skipped" "${skipped:-0}"
json_add_int "failed" "${failed:-0}"
json_add_string "output" "$result"
json_dump
}
# Deploy ttyd to node(s) - shortcut
method_deploy_ttyd() {
local target
read -r input
json_load "$input"
json_get_var target target
[ -z "$target" ] && {
echo '{"success":false,"error":"Missing target (node_id or \"all\")"}'
return
}
# Execute via rttyctl
local result=$($RTTYCTL deploy-ttyd "$target" 2>&1)
local rc=$?
json_init
if [ $rc -eq 0 ] && echo "$result" | grep -q "✓"; then
json_add_boolean "success" 1
json_add_string "message" "ttyd deployed to $target"
# If single node, include ttyd URL
if [ "$target" != "all" ]; then
local addr=$($RTTYCTL node "$target" 2>&1 | grep "Address:" | awk '{print $2}')
[ -z "$addr" ] && addr="$target"
json_add_string "terminal_url" "http://${addr}:7681/"
fi
else
json_add_boolean "success" 0
json_add_string "error" "$result"
fi
json_dump
}
# Check package status on remote node
method_install_status() {
local node_id app_id
read -r input
json_load "$input"
json_get_var node_id node_id
json_get_var app_id app_id
[ -z "$node_id" ] && {
echo '{"success":false,"error":"Missing node_id"}'
return
}
local result=$($RTTYCTL install-status "$node_id" "$app_id" 2>&1)
json_init
json_add_boolean "success" 1
json_add_string "node_id" "$node_id"
json_add_string "output" "$result"
json_dump
}
#------------------------------------------------------------------------------
# Avatar-Tap Session Integration
#------------------------------------------------------------------------------
# Get Avatar-Tap status
method_get_tap_status() {
local running=0
pgrep -f "mitmdump.*tap.py" >/dev/null && running=1
# Get database path from UCI
config_load avatar-tap
local db_path
config_get db_path main db_path '/srv/avatar-tap/sessions.db'
local sessions=0
local recent=0
if [ -f "$db_path" ]; then
sessions=$(sqlite3 "$db_path" "SELECT COUNT(*) FROM sessions" 2>/dev/null || echo 0)
recent=$(sqlite3 "$db_path" "SELECT COUNT(*) FROM sessions WHERE captured_at > strftime('%s','now','-1 hour')" 2>/dev/null || echo 0)
fi
json_init
json_add_boolean "running" "$running"
json_add_int "sessions" "$sessions"
json_add_int "recent" "$recent"
json_add_string "db_path" "$db_path"
json_dump
}
# 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
#------------------------------------------------------------------------------
case "$1" in
list)
cat << 'EOF'
{
"status": {},
"get_nodes": {},
"get_node": {"node_id": "string"},
"rpc_call": {"node_id": "string", "object": "string", "method": "string", "params": "string"},
"rpc_list": {"node_id": "string"},
"get_sessions": {"node_id": "string", "limit": 50},
"server_start": {},
"server_stop": {},
"get_settings": {},
"set_settings": {"config": "object"},
"replay_session": {"session_id": "integer", "target_node": "string"},
"connect": {"node_id": "string"},
"start_terminal": {"node_id": "string"},
"token_generate": {"ttl": 3600, "permissions": "rpc,terminal"},
"token_list": {},
"token_validate": {"code": "string"},
"token_revoke": {"code": "string"},
"token_rpc": {"code": "string", "object": "string", "method": "string", "params": "string"},
"get_tap_sessions": {},
"get_tap_session": {"session_id": "integer"},
"replay_to_node": {"session_id": "integer", "target_node": "string"},
"export_session": {"session_id": "integer"},
"import_session": {"content": "string"},
"get_tap_status": {},
"install_remote": {"node_id": "string", "app_id": "string"},
"install_mesh": {"app_id": "string"},
"deploy_ttyd": {"target": "string"},
"install_status": {"node_id": "string", "app_id": "string"}
}
EOF
;;
call)
case "$2" in
status) method_status ;;
get_nodes) method_get_nodes ;;
get_node) method_get_node ;;
rpc_call) method_rpc_call ;;
rpc_list) method_rpc_list ;;
get_sessions) method_get_sessions ;;
server_start) method_server_start ;;
server_stop) method_server_stop ;;
get_settings) method_get_settings ;;
set_settings) method_set_settings ;;
replay_session) method_replay_session ;;
connect) method_connect ;;
start_terminal) method_start_terminal ;;
token_generate) method_token_generate ;;
token_list) method_token_list ;;
token_validate) method_token_validate ;;
token_revoke) method_token_revoke ;;
token_rpc) method_token_rpc ;;
get_tap_sessions) method_get_tap_sessions ;;
get_tap_session) method_get_tap_session ;;
replay_to_node) method_replay_to_node ;;
export_session) method_export_session ;;
import_session) method_import_session ;;
get_tap_status) method_get_tap_status ;;
install_remote) method_install_remote ;;
install_mesh) method_install_mesh ;;
deploy_ttyd) method_deploy_ttyd ;;
install_status) method_install_status ;;
*)
echo '{"error":"Unknown method"}'
;;
esac
;;
esac