#!/bin/sh # # RPCD backend for SecuBox Service Exposure Manager # . /usr/share/libubox/jshn.sh . /lib/functions.sh case "$1" in list) json_init json_add_object "scan" json_close_object json_add_object "conflicts" json_close_object json_add_object "status" json_close_object json_add_object "tor_list" json_close_object json_add_object "ssl_list" json_close_object json_add_object "get_config" json_close_object json_add_object "fix_port" json_add_string "service" "string" json_add_int "port" "integer" json_close_object json_add_object "tor_add" json_add_string "service" "string" json_add_int "local_port" "integer" json_add_int "onion_port" "integer" json_close_object json_add_object "tor_remove" json_add_string "service" "string" json_close_object json_add_object "ssl_add" json_add_string "service" "string" json_add_string "domain" "string" json_add_int "local_port" "integer" json_close_object json_add_object "ssl_remove" json_add_string "service" "string" json_close_object json_add_object "vhost_list" json_close_object json_add_object "emancipate" json_add_string "service" "string" json_add_int "port" "integer" json_add_string "domain" "string" json_add_boolean "tor" "boolean" json_add_boolean "dns" "boolean" json_add_boolean "mesh" "boolean" json_close_object json_add_object "revoke" json_add_string "service" "string" json_add_boolean "tor" "boolean" json_add_boolean "dns" "boolean" json_add_boolean "mesh" "boolean" json_close_object json_add_object "get_emancipated" json_close_object json_dump ;; call) case "$2" in scan) # Scan listening services - use temp file to avoid subshell issues TMP_SVC="/tmp/exposure_scan_$$" TMP_NAMES="/tmp/exposure_names_$$" > "$TMP_NAMES" netstat -tlnp 2>/dev/null | grep LISTEN | awk '{ split($4, a, ":") port = a[length(a)] if (!seen[port]++) { split($7, p, "/") proc = p[2] if (proc == "") proc = "unknown" print port, $4, proc } }' | sort -n > "$TMP_SVC" # Build port->name enrichment from component configs # uhttpd instances for _s in $(uci show uhttpd 2>/dev/null | grep "=uhttpd$" | cut -d'.' -f2 | cut -d'=' -f1); do _listen=$(uci -q get "uhttpd.${_s}.listen_http") [ -z "$_listen" ] && continue _p=$(echo "$_listen" | grep -o '[0-9]*$') case "$_s" in main) echo "$_p|LuCI" >> "$TMP_NAMES" ;; acme) echo "$_p|ACME Challenge" >> "$TMP_NAMES" ;; metablog_site_*) echo "$_p|Metablog: $(echo "$_s" | sed 's/^metablog_site_//')" >> "$TMP_NAMES" ;; p2p_api) echo "$_p|P2P API" >> "$TMP_NAMES" ;; *) echo "$_p|uhttpd: $_s" >> "$TMP_NAMES" ;; esac done # Streamlit instances for _s in $(uci show streamlit 2>/dev/null | grep "\.port=" | cut -d'.' -f2); do _p=$(uci -q get "streamlit.${_s}.port") _n=$(uci -q get "streamlit.${_s}.name") [ -n "$_p" ] && echo "$_p|Streamlit: ${_n:-$_s}" >> "$TMP_NAMES" done # Docker containers docker ps --format '{{.Ports}}|{{.Names}}' 2>/dev/null | while IFS='|' read _ports _cname; do [ -z "$_cname" ] && continue echo "$_ports" | tr ',' '\n' | while read _bind; do _hp=$(echo "$_bind" | sed -n 's/.*:\([0-9]*\)->.*/\1/p') [ -n "$_hp" ] && echo "$_hp|Docker: $_cname" >> "$TMP_NAMES" done done # Glances _gp=$(uci -q get glances.main.web_port) [ -n "$_gp" ] && echo "$_gp|Glances" >> "$TMP_NAMES" # Known services by port echo "9000|Lyrion" >> "$TMP_NAMES" echo "3483|Lyrion Discovery" >> "$TMP_NAMES" echo "9090|Lyrion CLI" >> "$TMP_NAMES" json_init json_add_array "services" while read port addr proc; do [ -z "$port" ] && continue external=0 case "$addr" in *0.0.0.0*|*::*) external=1 ;; *127.0.0.1*|*::1*) external=0 ;; *) external=1 ;; esac # Try enriched name first, fallback to process-based mapping name=$(grep "^${port}|" "$TMP_NAMES" | head -1 | cut -d'|' -f2) if [ -z "$name" ]; then case "$proc" in sshd|dropbear) name="SSH" ;; dnsmasq) name="DNS" ;; haproxy) name="HAProxy" ;; uhttpd) name="LuCI" ;; gitea) name="Gitea" ;; netifyd) name="Netifyd" ;; tor) name="Tor" ;; python*) name="Python App" ;; streamlit) name="Streamlit" ;; hexo|node) name="HexoJS" ;; *) name="$proc" ;; esac fi json_add_object "" json_add_int "port" "$port" json_add_string "address" "$addr" json_add_string "process" "$proc" json_add_string "name" "$name" json_add_boolean "external" "$external" json_close_object done < "$TMP_SVC" rm -f "$TMP_SVC" "$TMP_NAMES" json_close_array json_dump ;; status) json_init total=$(netstat -tlnp 2>/dev/null | grep LISTEN | awk '{split($4,a,":"); print a[length(a)]}' | sort -u | wc -l) external=$(netstat -tlnp 2>/dev/null | grep LISTEN | grep -E "0\.0\.0\.0|::" | awk '{split($4,a,":"); print a[length(a)]}' | sort -u | wc -l) json_add_object "services" json_add_int "total" "$total" json_add_int "external" "$external" json_close_object # Tor hidden services TOR_DIR="/var/lib/tor/hidden_services" tor_count=0 [ -d "$TOR_DIR" ] && tor_count=$(ls -1d "$TOR_DIR"/*/ 2>/dev/null | wc -l) json_add_object "tor" json_add_int "count" "$tor_count" json_add_array "services" if [ -d "$TOR_DIR" ]; then for dir in "$TOR_DIR"/*/; do [ -d "$dir" ] || continue svc=$(basename "$dir") onion="" [ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname") if [ -n "$onion" ]; then json_add_object "" json_add_string "service" "$svc" json_add_string "onion" "$onion" json_close_object fi done fi json_close_array json_close_object # HAProxy SSL backends - read from UCI config TMP_SSL="/tmp/exposure_ssl_$$" ssl_count=0 # Get vhosts from UCI (enabled ones with domains) for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do domain=$(uci -q get "haproxy.${vhost}.domain") backend=$(uci -q get "haproxy.${vhost}.backend") enabled=$(uci -q get "haproxy.${vhost}.enabled") [ "$enabled" != "1" ] && continue [ -z "$domain" ] && continue echo "${backend:-$vhost}|${domain}" >> "$TMP_SSL" ssl_count=$((ssl_count + 1)) done json_add_object "ssl" json_add_int "count" "$ssl_count" json_add_array "backends" if [ -f "$TMP_SSL" ]; then while IFS='|' read backend domain; do [ -z "$backend" ] && continue json_add_object "" json_add_string "service" "$backend" json_add_string "domain" "$domain" json_close_object done < "$TMP_SSL" rm -f "$TMP_SSL" fi json_close_array json_close_object json_dump ;; tor_list) TOR_DIR="/var/lib/tor/hidden_services" TOR_CONFIG="/etc/tor/torrc" json_init json_add_array "services" if [ -d "$TOR_DIR" ]; then for dir in "$TOR_DIR"/*/; do [ -d "$dir" ] || continue svc=$(basename "$dir") onion="" [ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname") port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $2}') backend=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $3}') if [ -n "$onion" ]; then json_add_object "" json_add_string "service" "$svc" json_add_string "onion" "$onion" json_add_string "port" "${port:-80}" json_add_string "backend" "${backend:-N/A}" json_close_object fi done fi json_close_array json_dump ;; ssl_list) # OPTIMIZED: Single-pass awk parsing uci show haproxy 2>/dev/null | awk ' BEGIN { printf "{\"backends\":[" first = 1 } /=vhost$/ { if (vh_domain != "" && vh_enabled == "1") { if (first == 0) printf "," first = 0 printf "{\"service\":\"%s\",\"domain\":\"%s\",\"backend\":\"%s\"}", (vh_backend != "" ? vh_backend : vh_id), vh_domain, "N/A" } gsub(/^haproxy\./, "", $0) gsub(/=vhost$/, "", $0) vh_id = $0 vh_domain = "" vh_backend = "" vh_enabled = "0" in_vhost = 1 } /=backend$/ || /=server$/ { in_vhost = 0 } /\.domain=/ && in_vhost { gsub(/.*\.domain=/, "", $0) gsub(/'\''/, "", $0) vh_domain = $0 } /\.backend=/ && in_vhost { gsub(/.*\.backend=/, "", $0) gsub(/'\''/, "", $0) vh_backend = $0 } /\.enabled=/ && in_vhost { gsub(/.*\.enabled=/, "", $0) gsub(/'\''/, "", $0) vh_enabled = $0 } END { if (vh_domain != "" && vh_enabled == "1") { if (first == 0) printf "," printf "{\"service\":\"%s\",\"domain\":\"%s\",\"backend\":\"%s\"}", (vh_backend != "" ? vh_backend : vh_id), vh_domain, "N/A" } printf "]}" } ' ;; get_config) json_init json_add_array "known_services" config_load "secubox-exposure" get_known() { local section="$1" local default_port config_path category config_get default_port "$section" default_port config_get config_path "$section" config_path config_get category "$section" category "other" actual_port="" if [ -n "$config_path" ]; then actual_port=$(uci -q get "$config_path" 2>/dev/null) fi [ -z "$actual_port" ] && actual_port="$default_port" json_add_object "" json_add_string "id" "$section" json_add_int "default_port" "${default_port:-0}" json_add_int "actual_port" "${actual_port:-0}" json_add_string "config_path" "$config_path" json_add_string "category" "$category" json_close_object } config_foreach get_known known json_close_array json_dump ;; conflicts) json_init json_add_array "conflicts" json_close_array json_dump ;; fix_port) read -r input service=$(echo "$input" | jsonfilter -e '@.service') port=$(echo "$input" | jsonfilter -e '@.port') if [ -z "$service" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Service name required" json_dump exit 0 fi result=$(/usr/sbin/secubox-exposure fix-port "$service" "$port" 2>&1) json_init if [ $? -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 ;; tor_add) read -r input service=$(echo "$input" | jsonfilter -e '@.service') local_port=$(echo "$input" | jsonfilter -e '@.local_port') onion_port=$(echo "$input" | jsonfilter -e '@.onion_port') if [ -z "$service" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Service name required" json_dump exit 0 fi result=$(/usr/sbin/secubox-exposure tor add "$service" "$local_port" "$onion_port" 2>&1) json_init if echo "$result" | grep -q "Hidden service created"; then onion=$(echo "$result" | grep "Onion:" | awk '{print $2}') json_add_boolean "success" 1 json_add_string "onion" "$onion" json_add_string "message" "Hidden service created" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump ;; tor_remove) read -r input service=$(echo "$input" | jsonfilter -e '@.service') if [ -z "$service" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Service name required" json_dump exit 0 fi result=$(/usr/sbin/secubox-exposure tor remove "$service" 2>&1) json_init if echo "$result" | grep -q "removed"; then json_add_boolean "success" 1 json_add_string "message" "Hidden service removed" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump ;; ssl_add) read -r input service=$(echo "$input" | jsonfilter -e '@.service') domain=$(echo "$input" | jsonfilter -e '@.domain') local_port=$(echo "$input" | jsonfilter -e '@.local_port') if [ -z "$service" ] || [ -z "$domain" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Service and domain required" json_dump exit 0 fi result=$(/usr/sbin/secubox-exposure ssl add "$service" "$domain" "$local_port" 2>&1) json_init if echo "$result" | grep -q "configured"; then json_add_boolean "success" 1 json_add_string "message" "SSL backend configured" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump ;; ssl_remove) read -r input service=$(echo "$input" | jsonfilter -e '@.service') if [ -z "$service" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Service name required" json_dump exit 0 fi result=$(/usr/sbin/secubox-exposure ssl remove "$service" 2>&1) json_init if echo "$result" | grep -q "removed"; then json_add_boolean "success" 1 json_add_string "message" "SSL backend removed" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump ;; vhost_list) # OPTIMIZED: Single-pass awk parsing instead of O(n²) uci calls uci show haproxy 2>/dev/null | awk ' BEGIN { printf "{\"haproxy\":[" first_vh = 1 } # Collect server backend->port mappings /\.backend=/ && /=server$/ == 0 { # This is a vhost or backend .backend= line, skip for server collection } /=server$/ { gsub(/^haproxy\./, "", $0) gsub(/=server$/, "", $0) current_srv = $0 } /^haproxy\.[^.]+\.backend=/ && prev_type == "server" { gsub(/^haproxy\.[^.]+\.backend=/, "", $0) gsub(/'\''/, "", $0) srv_backends[current_srv] = $0 } /^haproxy\.[^.]+\.port=/ && prev_type == "server" { gsub(/^haproxy\.[^.]+\.port=/, "", $0) gsub(/'\''/, "", $0) srv_ports[current_srv] = $0 } # Track section type /=vhost$/ { prev_type = "vhost" } /=server$/ { prev_type = "server" } /=backend$/ { prev_type = "backend" } # Process vhosts /=vhost$/ { # Output previous vhost if (vh_id != "" && vh_domain != "") { if (first_vh == 0) printf "," first_vh = 0 # Resolve backend port resolve_be = (vh_orig_be != "") ? vh_orig_be : vh_backend port = 0 for (s in srv_backends) { if (srv_backends[s] == resolve_be && srv_ports[s] != "") { port = srv_ports[s] break } } printf "{\"id\":\"%s\",\"domain\":\"%s\",\"backend\":\"%s\",\"backend_port\":%d,\"ssl\":%s,\"acme\":%s,\"enabled\":%s}", vh_id, vh_domain, resolve_be, port, (vh_ssl == "1" ? "true" : "false"), (vh_acme == "1" ? "true" : "false"), (vh_enabled == "1" ? "true" : "false") } # Start new vhost gsub(/^haproxy\./, "", $0) gsub(/=vhost$/, "", $0) vh_id = $0 vh_domain = "" vh_backend = "" vh_orig_be = "" vh_ssl = "0" vh_acme = "0" vh_enabled = "0" } /\.domain=/ && prev_type == "vhost" { gsub(/.*\.domain=/, "", $0) gsub(/'\''/, "", $0) vh_domain = $0 } /\.backend=/ && prev_type == "vhost" { gsub(/.*\.backend=/, "", $0) gsub(/'\''/, "", $0) vh_backend = $0 } /\.original_backend=/ && prev_type == "vhost" { gsub(/.*\.original_backend=/, "", $0) gsub(/'\''/, "", $0) vh_orig_be = $0 } /\.ssl=/ && prev_type == "vhost" { gsub(/.*\.ssl=/, "", $0) gsub(/'\''/, "", $0) vh_ssl = $0 } /\.acme=/ && prev_type == "vhost" { gsub(/.*\.acme=/, "", $0) gsub(/'\''/, "", $0) vh_acme = $0 } /\.enabled=/ && prev_type == "vhost" { gsub(/.*\.enabled=/, "", $0) gsub(/'\''/, "", $0) vh_enabled = $0 } END { # Output last vhost if (vh_id != "" && vh_domain != "") { if (first_vh == 0) printf "," resolve_be = (vh_orig_be != "") ? vh_orig_be : vh_backend port = 0 for (s in srv_backends) { if (srv_backends[s] == resolve_be && srv_ports[s] != "") { port = srv_ports[s] break } } printf "{\"id\":\"%s\",\"domain\":\"%s\",\"backend\":\"%s\",\"backend_port\":%d,\"ssl\":%s,\"acme\":%s,\"enabled\":%s}", vh_id, vh_domain, resolve_be, port, (vh_ssl == "1" ? "true" : "false"), (vh_acme == "1" ? "true" : "false"), (vh_enabled == "1" ? "true" : "false") } printf "]," } ' # uhttpd vhosts - also optimized with awk uci show uhttpd 2>/dev/null | awk ' BEGIN { printf "\"uhttpd\":[" first = 1 } /=uhttpd$/ { if (section != "" && section != "main" && section != "acme" && listen != "") { if (first == 0) printf "," first = 0 # Extract port from listen match(listen, /[0-9]+$/) port = substr(listen, RSTART, RLENGTH) gsub(/^metablog_site_/, "", section) gsub(/_/, " ", section) printf "{\"id\":\"%s\",\"port\":%s,\"name\":\"%s\",\"home\":\"%s\"}", orig_section, (port != "" ? port : "0"), section, home } gsub(/^uhttpd\./, "", $0) gsub(/=uhttpd$/, "", $0) orig_section = $0 section = $0 listen = "" home = "" } /\.listen_http=/ { gsub(/.*\.listen_http=/, "", $0) gsub(/'\''/, "", $0) listen = $0 } /\.home=/ { gsub(/.*\.home=/, "", $0) gsub(/'\''/, "", $0) home = $0 } END { if (section != "" && section != "main" && section != "acme" && listen != "") { if (first == 0) printf "," match(listen, /[0-9]+$/) port = substr(listen, RSTART, RLENGTH) gsub(/^metablog_site_/, "", section) gsub(/_/, " ", section) printf "{\"id\":\"%s\",\"port\":%s,\"name\":\"%s\",\"home\":\"%s\"}", orig_section, (port != "" ? port : "0"), section, home } printf "]}" } ' ;; emancipate) read -r input service=$(echo "$input" | jsonfilter -e '@.service') port=$(echo "$input" | jsonfilter -e '@.port') domain=$(echo "$input" | jsonfilter -e '@.domain') tor=$(echo "$input" | jsonfilter -e '@.tor') dns=$(echo "$input" | jsonfilter -e '@.dns') mesh=$(echo "$input" | jsonfilter -e '@.mesh') if [ -z "$service" ] || [ -z "$port" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Service and port required" json_dump exit 0 fi flags="" [ "$tor" = "true" ] || [ "$tor" = "1" ] && flags="$flags --tor" [ "$dns" = "true" ] || [ "$dns" = "1" ] && flags="$flags --dns" [ "$mesh" = "true" ] || [ "$mesh" = "1" ] && flags="$flags --mesh" [ -z "$flags" ] && flags="--all" result=$(/usr/sbin/secubox-exposure emancipate "$service" "$port" "$domain" $flags 2>&1) rc=$? json_init if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "Service emancipated" json_add_string "output" "$result" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump ;; revoke) read -r input service=$(echo "$input" | jsonfilter -e '@.service') tor=$(echo "$input" | jsonfilter -e '@.tor') dns=$(echo "$input" | jsonfilter -e '@.dns') mesh=$(echo "$input" | jsonfilter -e '@.mesh') if [ -z "$service" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Service name required" json_dump exit 0 fi flags="" [ "$tor" = "true" ] || [ "$tor" = "1" ] && flags="$flags --tor" [ "$dns" = "true" ] || [ "$dns" = "1" ] && flags="$flags --dns" [ "$mesh" = "true" ] || [ "$mesh" = "1" ] && flags="$flags --mesh" [ -z "$flags" ] && flags="--all" result=$(/usr/sbin/secubox-exposure revoke "$service" $flags 2>&1) rc=$? json_init if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "Service revoked" json_add_string "output" "$result" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump ;; get_emancipated) json_init json_add_array "services" # Read emancipated services from UCI for svc in $(uci show secubox-exposure 2>/dev/null | grep "=service$" | cut -d'.' -f2 | cut -d'=' -f1); do emancipated=$(uci -q get "secubox-exposure.$svc.emancipated") [ "$emancipated" != "1" ] && continue port=$(uci -q get "secubox-exposure.$svc.port") domain=$(uci -q get "secubox-exposure.$svc.domain") tor=$(uci -q get "secubox-exposure.$svc.tor") dns=$(uci -q get "secubox-exposure.$svc.dns") mesh=$(uci -q get "secubox-exposure.$svc.mesh") json_add_object "" json_add_string "name" "$svc" json_add_int "port" "${port:-0}" json_add_string "domain" "${domain:-}" json_add_boolean "tor" "${tor:-0}" json_add_boolean "dns" "${dns:-0}" json_add_boolean "mesh" "${mesh:-0}" json_close_object done json_close_array json_dump ;; *) json_init json_add_boolean "error" 1 json_add_string "message" "Unknown method: $2" json_dump ;; esac ;; esac