#!/bin/sh # SPDX-License-Identifier: MIT # Service Registry RPCD backend # Copyright (C) 2025 CyberMind.fr . /lib/functions.sh . /usr/share/libubox/jshn.sh UCI_CONFIG="service-registry" TOR_DATA="/var/lib/tor" LAN_IP="192.168.255.1" # Helper: Get UCI value get_uci() { local section="$1" local option="$2" local default="$3" local value value=$(uci -q get "$UCI_CONFIG.$section.$option") echo "${value:-$default}" } # Helper: Get LAN IP address get_lan_ip() { local ip ip=$(uci -q get network.lan.ipaddr) [ -z "$ip" ] && ip=$(ip -4 addr show br-lan 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1) echo "${ip:-$LAN_IP}" } # Helper: Check if port is listening is_port_listening() { local port="$1" netstat -tln 2>/dev/null | grep -q ":${port} " && return 0 return 1 } # Helper: Check if HAProxy is available and running haproxy_available() { # Check if HAProxy container is running (preferred method) if command -v lxc-info >/dev/null 2>&1; then lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING" && return 0 fi # Fallback: check if haproxy process is running pgrep haproxy >/dev/null 2>&1 && return 0 # Fallback: check if RPCD haproxy interface is available ubus call luci.haproxy list 2>/dev/null | jsonfilter -e '@' >/dev/null 2>&1 && return 0 return 1 } # Helper: Get process name for port get_process_for_port() { local port="$1" netstat -tlnp 2>/dev/null | grep ":${port} " | awk '{print $7}' | cut -d'/' -f2 | head -1 } # Helper: Ensure firewall rules for HAProxy (HTTP/HTTPS from WAN) ensure_haproxy_firewall_rules() { local http_port="${1:-80}" local https_port="${2:-443}" local changed=0 # Check and create HTTP rule local http_exists=0 local i=0 while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do local name=$(uci -q get firewall.@rule[$i].name) [ "$name" = "HAProxy-HTTP" ] && http_exists=1 && break i=$((i + 1)) done if [ "$http_exists" = "0" ]; then uci add firewall rule >/dev/null uci set firewall.@rule[-1].name='HAProxy-HTTP' uci set firewall.@rule[-1].src='wan' uci set firewall.@rule[-1].dest_port="$http_port" uci set firewall.@rule[-1].proto='tcp' uci set firewall.@rule[-1].target='ACCEPT' uci set firewall.@rule[-1].enabled='1' changed=1 fi # Check and create HTTPS rule local https_exists=0 i=0 while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do local name=$(uci -q get firewall.@rule[$i].name) [ "$name" = "HAProxy-HTTPS" ] && https_exists=1 && break i=$((i + 1)) done if [ "$https_exists" = "0" ]; then uci add firewall rule >/dev/null uci set firewall.@rule[-1].name='HAProxy-HTTPS' uci set firewall.@rule[-1].src='wan' uci set firewall.@rule[-1].dest_port="$https_port" uci set firewall.@rule[-1].proto='tcp' uci set firewall.@rule[-1].target='ACCEPT' uci set firewall.@rule[-1].enabled='1' changed=1 fi # Apply firewall changes if [ "$changed" = "1" ]; then uci commit firewall /etc/init.d/firewall reload >/dev/null 2>&1 & fi } # List all services aggregated from providers method_list_services() { local lan_ip lan_ip=$(get_lan_ip) json_init json_add_array "services" # Temporary file for service aggregation (avoid subshell issues) local TMP_SERVICES="/tmp/sr_services_$$" > "$TMP_SERVICES" # 1. Get published services from UCI config_load "$UCI_CONFIG" config_foreach _add_published_service service "$lan_ip" "$TMP_SERVICES" # 2. Get HAProxy vhosts local haproxy_enabled haproxy_enabled=$(get_uci haproxy enabled 1) if [ "$haproxy_enabled" = "1" ]; then _aggregate_haproxy_services "$lan_ip" "$TMP_SERVICES" fi # 3. Get Tor hidden services local tor_enabled tor_enabled=$(get_uci tor enabled 1) if [ "$tor_enabled" = "1" ]; then _aggregate_tor_services "$TMP_SERVICES" fi # 4. Get direct listening services (from luci.secubox) # NOTE: Disabled by default - too slow with many ports (spawns jsonfilter per service) # Enable in UCI: uci set service-registry.direct.enabled=1 local direct_enabled direct_enabled=$(get_uci direct enabled 0) if [ "$direct_enabled" = "1" ]; then _aggregate_direct_services "$lan_ip" "$TMP_SERVICES" fi # 5. Get LXC container services local lxc_enabled lxc_enabled=$(get_uci lxc enabled 1) if [ "$lxc_enabled" = "1" ]; then _aggregate_lxc_services "$lan_ip" "$TMP_SERVICES" fi rm -f "$TMP_SERVICES" json_close_array # Provider status json_add_object "providers" json_add_object "haproxy" if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then json_add_string "status" "running" else json_add_string "status" "stopped" fi local haproxy_count haproxy_count=$(uci -q show haproxy | grep -c "=vhost$") json_add_int "count" "${haproxy_count:-0}" json_close_object json_add_object "tor" if pgrep -f "/usr/sbin/tor" >/dev/null 2>&1; then json_add_string "status" "running" else json_add_string "status" "stopped" fi local tor_count tor_count=$(uci -q show tor-shield | grep -c "=hidden_service$") json_add_int "count" "${tor_count:-0}" json_close_object json_add_object "direct" local direct_count direct_count=$(netstat -tln 2>/dev/null | grep -c LISTEN) json_add_int "count" "${direct_count:-0}" json_close_object json_add_object "lxc" local lxc_running lxc_running=$(lxc-ls --running 2>/dev/null | wc -w) json_add_int "count" "${lxc_running:-0}" json_close_object json_close_object json_dump } # Add published service from UCI _add_published_service() { local section="$1" local lan_ip="$2" local tmp_file="$3" local name category icon local_port published local haproxy_enabled haproxy_domain haproxy_ssl local tor_enabled tor_onion tor_port config_get name "$section" name "$section" config_get category "$section" category "services" config_get icon "$section" icon "" config_get local_port "$section" local_port "" config_get published "$section" published "0" [ "$published" != "1" ] && return [ -z "$local_port" ] && return config_get haproxy_enabled "$section" haproxy_enabled "0" config_get haproxy_domain "$section" haproxy_domain "" config_get haproxy_ssl "$section" haproxy_ssl "0" config_get tor_enabled "$section" tor_enabled "0" config_get tor_onion "$section" tor_onion "" config_get tor_port "$section" tor_port "80" # Check if service is running local status="stopped" is_port_listening "$local_port" && status="running" json_add_object json_add_string "id" "$section" json_add_string "name" "$name" json_add_string "category" "$category" json_add_string "icon" "$icon" json_add_int "local_port" "$local_port" json_add_string "status" "$status" json_add_boolean "published" 1 # URLs json_add_object "urls" json_add_string "local" "http://${lan_ip}:${local_port}" [ "$haproxy_enabled" = "1" ] && [ -n "$haproxy_domain" ] && { if [ "$haproxy_ssl" = "1" ]; then json_add_string "clearnet" "https://${haproxy_domain}" else json_add_string "clearnet" "http://${haproxy_domain}" fi } [ "$tor_enabled" = "1" ] && [ -n "$tor_onion" ] && { json_add_string "onion" "http://${tor_onion}:${tor_port}" } json_close_object # HAProxy config json_add_object "haproxy" json_add_boolean "enabled" "$haproxy_enabled" [ -n "$haproxy_domain" ] && json_add_string "domain" "$haproxy_domain" json_add_boolean "ssl" "$haproxy_ssl" json_close_object # Tor config json_add_object "tor" json_add_boolean "enabled" "$tor_enabled" [ -n "$tor_onion" ] && json_add_string "onion_address" "$tor_onion" json_add_int "virtual_port" "$tor_port" json_close_object json_close_object # Mark as processed echo "$local_port" >> "$tmp_file" } # Aggregate HAProxy vhosts _aggregate_haproxy_services() { local lan_ip="$1" local tmp_file="$2" # Check if HAProxy is available before calling RPCD if ! haproxy_available; then return 0 fi # Call HAProxy RPCD to get vhosts and backends local vhosts_json backends_json certs_json vhosts_json=$(ubus call luci.haproxy list_vhosts 2>/dev/null) [ -z "$vhosts_json" ] && return backends_json=$(ubus call luci.haproxy list_backends 2>/dev/null) certs_json=$(ubus call luci.haproxy list_certificates 2>/dev/null) # Get array length local count count=$(echo "$vhosts_json" | jsonfilter -e '@.vhosts[*].domain' 2>/dev/null | wc -l) [ "$count" -eq 0 ] && return local i=0 while [ $i -lt "$count" ]; do local id domain backend ssl ssl_redirect acme enabled id=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].id" 2>/dev/null) domain=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].domain" 2>/dev/null) backend=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].backend" 2>/dev/null) ssl=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].ssl" 2>/dev/null) ssl_redirect=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].ssl_redirect" 2>/dev/null) acme=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].acme" 2>/dev/null) enabled=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].enabled" 2>/dev/null) i=$((i + 1)) # Skip if domain empty or disabled [ -z "$domain" ] && continue # Check if already processed grep -q "^haproxy_${domain}$" "$tmp_file" 2>/dev/null && continue # Get backend port from servers local backend_port="" if [ -n "$backends_json" ] && [ -n "$backend" ]; then backend_port=$(echo "$backends_json" | jsonfilter -e "@.backends[@.name='$backend'].servers[0].port" 2>/dev/null) [ -z "$backend_port" ] && backend_port=$(echo "$backends_json" | jsonfilter -e "@.backends[@.id='$backend'].servers[0].port" 2>/dev/null) fi # Check certificate status local cert_status="none" if [ "$ssl" = "true" ] || [ "$ssl" = "1" ]; then if [ "$acme" = "true" ] || [ "$acme" = "1" ]; then cert_status="acme" else cert_status="manual" fi fi # Determine status based on enabled and HAProxy running local status="stopped" if [ "$enabled" = "true" ] || [ "$enabled" = "1" ]; then # Check if HAProxy container is running if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then status="running" fi else status="disabled" fi json_add_object json_add_string "id" "$id" json_add_string "name" "$domain" json_add_string "category" "proxy" json_add_string "icon" "arrow" json_add_string "status" "$status" json_add_boolean "published" 1 json_add_string "source" "haproxy" # URLs json_add_object "urls" json_add_string "local" "http://${lan_ip}${backend_port:+:$backend_port}" if [ "$ssl" = "true" ] || [ "$ssl" = "1" ]; then json_add_string "clearnet" "https://${domain}" else json_add_string "clearnet" "http://${domain}" fi json_close_object # HAProxy details json_add_object "haproxy" json_add_string "id" "$id" json_add_string "domain" "$domain" json_add_string "backend" "$backend" [ -n "$backend_port" ] && json_add_int "backend_port" "$backend_port" if [ "$ssl" = "true" ] || [ "$ssl" = "1" ]; then json_add_boolean "ssl" 1 else json_add_boolean "ssl" 0 fi if [ "$ssl_redirect" = "true" ] || [ "$ssl_redirect" = "1" ]; then json_add_boolean "ssl_redirect" 1 else json_add_boolean "ssl_redirect" 0 fi if [ "$acme" = "true" ] || [ "$acme" = "1" ]; then json_add_boolean "acme" 1 else json_add_boolean "acme" 0 fi if [ "$enabled" = "true" ] || [ "$enabled" = "1" ]; then json_add_boolean "enabled" 1 else json_add_boolean "enabled" 0 fi json_add_string "cert_status" "$cert_status" json_close_object json_close_object echo "haproxy_${domain}" >> "$tmp_file" done } # Aggregate Tor hidden services _aggregate_tor_services() { local tmp_file="$1" # Get hidden services from tor-shield config config_load "tor-shield" config_foreach _add_tor_hidden_service hidden_service "$tmp_file" } _add_tor_hidden_service() { local section="$1" local tmp_file="$2" local enabled name local_port virtual_port config_get enabled "$section" enabled "0" [ "$enabled" != "1" ] && return config_get name "$section" name "$section" config_get local_port "$section" local_port "80" config_get virtual_port "$section" virtual_port "80" # Check if already processed grep -q "^tor_${name}$" "$tmp_file" 2>/dev/null && continue # Get onion address local hostname_file="$TOR_DATA/hidden_service_${name}/hostname" local onion_addr="" [ -f "$hostname_file" ] && onion_addr=$(cat "$hostname_file") json_add_object json_add_string "id" "tor_${name}" json_add_string "name" "${name} (Tor)" json_add_string "category" "privacy" json_add_string "icon" "onion" json_add_int "local_port" "$local_port" json_add_string "status" "running" json_add_boolean "published" 1 json_add_string "source" "tor" json_add_object "urls" [ -n "$onion_addr" ] && json_add_string "onion" "http://${onion_addr}:${virtual_port}" json_close_object json_add_object "tor" json_add_boolean "enabled" 1 json_add_string "onion_address" "$onion_addr" json_add_int "virtual_port" "$virtual_port" json_close_object json_close_object echo "tor_${name}" >> "$tmp_file" } # Aggregate direct listening services _aggregate_direct_services() { local lan_ip="$1" local tmp_file="$2" # Get services from luci.secubox get_services local services_json services_json=$(ubus call luci.secubox get_services 2>/dev/null) [ -z "$services_json" ] && return # Parse all services at once using jsonfilter list mode for better performance # Extract port|name|category|icon tuples in a single pass local data_file="/tmp/sr_direct_$$" echo "$services_json" | jsonfilter -e '@.services[*].port' -e '@.services[*].name' -e '@.services[*].category' -e '@.services[*].icon' > "$data_file" 2>/dev/null # Count services local count=0 local ports="" local names="" local categories="" local icons="" # Read ports line by line (each jsonfilter -e outputs one line per match) count=$(echo "$services_json" | jsonfilter -e '@.services[*].port' 2>/dev/null | wc -l) [ "$count" -eq 0 ] && { rm -f "$data_file"; return; } # Limit to first 20 direct services to avoid performance issues [ "$count" -gt 20 ] && count=20 # Get all values at once using array notation local i=0 while [ $i -lt "$count" ]; do local port name category icon port=$(echo "$services_json" | jsonfilter -e "@.services[$i].port" 2>/dev/null) name=$(echo "$services_json" | jsonfilter -e "@.services[$i].name" 2>/dev/null) category=$(echo "$services_json" | jsonfilter -e "@.services[$i].category" 2>/dev/null) icon=$(echo "$services_json" | jsonfilter -e "@.services[$i].icon" 2>/dev/null) i=$((i + 1)) [ -z "$port" ] && continue # Skip if already processed grep -q "^${port}$" "$tmp_file" 2>/dev/null && continue # Skip common system ports (often not user services) case "$port" in 22|53|67|68|123|547|953) continue ;; esac json_add_object json_add_string "id" "direct_${port}" json_add_string "name" "${name:-Port $port}" json_add_string "category" "${category:-other}" json_add_string "icon" "$icon" json_add_int "local_port" "$port" json_add_string "status" "running" json_add_boolean "published" 0 json_add_string "source" "direct" json_add_object "urls" json_add_string "local" "http://${lan_ip}:${port}" json_close_object json_close_object echo "$port" >> "$tmp_file" done rm -f "$data_file" } # Aggregate LXC container services _aggregate_lxc_services() { local lan_ip="$1" local tmp_file="$2" # Get running containers local containers containers=$(lxc-ls --running 2>/dev/null) [ -z "$containers" ] && return for container in $containers; do # Skip if already processed grep -q "^lxc_${container}$" "$tmp_file" 2>/dev/null && continue # Get container IP local container_ip container_ip=$(lxc-info -n "$container" -iH 2>/dev/null | head -1) json_add_object json_add_string "id" "lxc_${container}" json_add_string "name" "$container" json_add_string "category" "container" json_add_string "icon" "box" json_add_string "status" "running" json_add_boolean "published" 0 json_add_string "source" "lxc" json_add_object "urls" [ -n "$container_ip" ] && json_add_string "local" "http://${container_ip}" json_close_object json_add_object "container" json_add_string "name" "$container" [ -n "$container_ip" ] && json_add_string "ip" "$container_ip" json_close_object json_close_object echo "lxc_${container}" >> "$tmp_file" done } # Get single service method_get_service() { local service_id read -r input json_load "$input" json_get_var service_id service_id json_init if [ -z "$service_id" ]; then json_add_boolean "success" 0 json_add_string "error" "service_id is required" json_dump return fi # Check if it's a published service if uci -q get "$UCI_CONFIG.$service_id" >/dev/null 2>&1; then local lan_ip lan_ip=$(get_lan_ip) json_add_boolean "success" 1 config_load "$UCI_CONFIG" _add_published_service "$service_id" "$lan_ip" "/dev/null" else json_add_boolean "success" 0 json_add_string "error" "Service not found" fi json_dump } # Publish a service method_publish_service() { local name local_port domain tor_enabled category icon read -r input json_load "$input" json_get_var name name json_get_var local_port local_port json_get_var domain domain "" json_get_var tor_enabled tor_enabled "0" json_get_var category category "services" json_get_var icon icon "" json_init if [ -z "$name" ] || [ -z "$local_port" ]; then json_add_boolean "success" 0 json_add_string "error" "name and local_port are required" json_dump return fi # Sanitize name for section ID local section_id section_id=$(echo "$name" | tr -cd 'a-zA-Z0-9_-' | tr '[:upper:]' '[:lower:]') # Create UCI service entry uci set "$UCI_CONFIG.$section_id=service" uci set "$UCI_CONFIG.$section_id.name=$name" uci set "$UCI_CONFIG.$section_id.local_port=$local_port" uci set "$UCI_CONFIG.$section_id.category=$category" [ -n "$icon" ] && uci set "$UCI_CONFIG.$section_id.icon=$icon" uci set "$UCI_CONFIG.$section_id.published=1" local lan_ip lan_ip=$(get_lan_ip) local urls_local="http://${lan_ip}:${local_port}" local urls_clearnet="" local urls_onion="" # Create HAProxy vhost if domain specified and HAProxy is available if [ -n "$domain" ]; then if haproxy_available; then # Ensure firewall allows HTTP/HTTPS from WAN (for public access + ACME) ensure_haproxy_firewall_rules # Create backend ubus call luci.haproxy create_backend "{\"name\":\"$section_id\",\"mode\":\"http\"}" 2>/dev/null # Create server pointing to local port (use LAN IP - HAProxy is in LXC container) local lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.255.1") ubus call luci.haproxy create_server "{\"backend\":\"$section_id\",\"name\":\"local\",\"address\":\"$lan_ip\",\"port\":$local_port}" 2>/dev/null # Create vhost with SSL ubus call luci.haproxy create_vhost "{\"domain\":\"$domain\",\"backend\":\"$section_id\",\"ssl\":1,\"ssl_redirect\":1,\"acme\":1,\"enabled\":1}" 2>/dev/null # Regenerate HAProxy config to include the new vhost ubus call luci.haproxy generate 2>/dev/null ubus call luci.haproxy reload 2>/dev/null uci set "$UCI_CONFIG.$section_id.haproxy_enabled=1" uci set "$UCI_CONFIG.$section_id.haproxy_domain=$domain" uci set "$UCI_CONFIG.$section_id.haproxy_ssl=1" urls_clearnet="https://${domain}" else # Store domain for later HAProxy configuration when it becomes available uci set "$UCI_CONFIG.$section_id.haproxy_enabled=0" uci set "$UCI_CONFIG.$section_id.haproxy_domain=$domain" uci set "$UCI_CONFIG.$section_id.haproxy_pending=1" logger -t service-registry "HAProxy unavailable, domain $domain saved for later" fi fi # Create Tor hidden service if enabled if [ "$tor_enabled" = "1" ]; then ubus call luci.tor-shield add_hidden_service "{\"name\":\"$section_id\",\"local_port\":$local_port,\"virtual_port\":80}" 2>/dev/null uci set "$UCI_CONFIG.$section_id.tor_enabled=1" uci set "$UCI_CONFIG.$section_id.tor_port=80" # Wait for onion address (max 5 seconds) local wait_count=0 local onion_addr="" while [ $wait_count -lt 5 ]; do sleep 1 local hostname_file="$TOR_DATA/hidden_service_${section_id}/hostname" if [ -f "$hostname_file" ]; then onion_addr=$(cat "$hostname_file") break fi wait_count=$((wait_count + 1)) done if [ -n "$onion_addr" ]; then uci set "$UCI_CONFIG.$section_id.tor_onion=$onion_addr" urls_onion="http://${onion_addr}:80" fi fi uci commit "$UCI_CONFIG" # Regenerate landing page if auto-regen enabled local auto_regen auto_regen=$(get_uci main landing_auto_regen 1) [ "$auto_regen" = "1" ] && /usr/sbin/secubox-landing-gen >/dev/null 2>&1 & json_add_boolean "success" 1 json_add_string "id" "$section_id" json_add_string "name" "$name" json_add_object "urls" json_add_string "local" "$urls_local" [ -n "$urls_clearnet" ] && json_add_string "clearnet" "$urls_clearnet" [ -n "$urls_onion" ] && json_add_string "onion" "$urls_onion" json_close_object json_dump } # Unpublish a service method_unpublish_service() { local service_id read -r input json_load "$input" json_get_var service_id service_id json_init if [ -z "$service_id" ]; then json_add_boolean "success" 0 json_add_string "error" "service_id is required" json_dump return fi # Check if service exists if ! uci -q get "$UCI_CONFIG.$service_id" >/dev/null 2>&1; then json_add_boolean "success" 0 json_add_string "error" "Service not found" json_dump return fi # Get service config config_load "$UCI_CONFIG" local haproxy_enabled haproxy_domain tor_enabled config_get haproxy_enabled "$service_id" haproxy_enabled "0" config_get haproxy_domain "$service_id" haproxy_domain "" config_get tor_enabled "$service_id" tor_enabled "0" # Remove HAProxy vhost (if HAProxy is available) if [ "$haproxy_enabled" = "1" ] && [ -n "$haproxy_domain" ]; then if haproxy_available; then local vhost_id vhost_id=$(echo "$haproxy_domain" | sed 's/[^a-zA-Z0-9]/_/g') ubus call luci.haproxy delete_vhost "{\"id\":\"$vhost_id\"}" 2>/dev/null ubus call luci.haproxy delete_backend "{\"id\":\"$service_id\"}" 2>/dev/null fi fi # Remove Tor hidden service if [ "$tor_enabled" = "1" ]; then ubus call luci.tor-shield remove_hidden_service "{\"name\":\"$service_id\"}" 2>/dev/null fi # Remove UCI entry uci delete "$UCI_CONFIG.$service_id" uci commit "$UCI_CONFIG" # Regenerate landing page local auto_regen auto_regen=$(get_uci main landing_auto_regen 1) [ "$auto_regen" = "1" ] && /usr/sbin/secubox-landing-gen >/dev/null 2>&1 & json_add_boolean "success" 1 json_add_string "message" "Service unpublished" json_dump } # Update service method_update_service() { local service_id name category icon read -r input json_load "$input" json_get_var service_id service_id json_get_var name name "" json_get_var category category "" json_get_var icon icon "" json_init if [ -z "$service_id" ]; then json_add_boolean "success" 0 json_add_string "error" "service_id is required" json_dump return fi if ! uci -q get "$UCI_CONFIG.$service_id" >/dev/null 2>&1; then json_add_boolean "success" 0 json_add_string "error" "Service not found" json_dump return fi [ -n "$name" ] && uci set "$UCI_CONFIG.$service_id.name=$name" [ -n "$category" ] && uci set "$UCI_CONFIG.$service_id.category=$category" [ -n "$icon" ] && uci set "$UCI_CONFIG.$service_id.icon=$icon" uci commit "$UCI_CONFIG" json_add_boolean "success" 1 json_dump } # Delete service method_delete_service() { method_unpublish_service } # Sync providers (refresh data) method_sync_providers() { json_init json_add_boolean "success" 1 json_add_string "message" "Providers synced" json_dump } # Generate landing page method_generate_landing_page() { json_init local result result=$(/usr/sbin/secubox-landing-gen 2>&1) local rc=$? if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "Landing page generated" json_add_string "path" "$(get_uci main landing_path '/www/secubox-services.html')" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump } # Get QR code data for a URL method_get_qr_data() { local service_id url_type read -r input json_load "$input" json_get_var service_id service_id json_get_var url_type url_type "local" json_init if [ -z "$service_id" ]; then json_add_boolean "success" 0 json_add_string "error" "service_id is required" json_dump return fi # Get service URL config_load "$UCI_CONFIG" local lan_ip local_port haproxy_domain haproxy_ssl tor_onion lan_ip=$(get_lan_ip) config_get local_port "$service_id" local_port "" config_get haproxy_domain "$service_id" haproxy_domain "" config_get haproxy_ssl "$service_id" haproxy_ssl "0" config_get tor_onion "$service_id" tor_onion "" local url="" case "$url_type" in local) [ -n "$local_port" ] && url="http://${lan_ip}:${local_port}" ;; clearnet) if [ -n "$haproxy_domain" ]; then if [ "$haproxy_ssl" = "1" ]; then url="https://${haproxy_domain}" else url="http://${haproxy_domain}" fi fi ;; onion) [ -n "$tor_onion" ] && url="http://${tor_onion}" ;; esac if [ -n "$url" ]; then json_add_boolean "success" 1 json_add_string "url" "$url" json_add_string "type" "$url_type" else json_add_boolean "success" 0 json_add_string "error" "URL not available for type: $url_type" fi json_dump } # List categories method_list_categories() { json_init json_add_array "categories" config_load "$UCI_CONFIG" config_foreach _add_category category json_close_array json_dump } _add_category() { local section="$1" local name icon order config_get name "$section" name "$section" config_get icon "$section" icon "" config_get order "$section" order "99" json_add_object json_add_string "id" "$section" json_add_string "name" "$name" json_add_string "icon" "$icon" json_add_int "order" "$order" json_close_object } # Helper: Check DNS resolution for a domain check_dns_resolution() { local domain="$1" local expected_ip="$2" # Try nslookup first (most common on OpenWrt) local resolved_ip if command -v nslookup >/dev/null 2>&1; then resolved_ip=$(nslookup "$domain" 2>/dev/null | grep -A1 "Name:" | grep "Address" | head -1 | awk '{print $2}') [ -z "$resolved_ip" ] && resolved_ip=$(nslookup "$domain" 2>/dev/null | grep "Address" | tail -1 | awk '{print $2}' | grep -v "^$") elif command -v host >/dev/null 2>&1; then resolved_ip=$(host "$domain" 2>/dev/null | grep "has address" | head -1 | awk '{print $4}') fi if [ -n "$resolved_ip" ]; then echo "$resolved_ip" return 0 fi return 1 } # Helper: Get WAN IP address (local interface) get_wan_ip() { local wan_ip="" # Try to get WAN IP from interface wan_ip=$(uci -q get network.wan.ipaddr) if [ -z "$wan_ip" ]; then # Try to get from ip command wan_ip=$(ip -4 addr show dev eth0 2>/dev/null | grep -oE 'inet [0-9.]+' | head -1 | awk '{print $2}') fi if [ -z "$wan_ip" ]; then # Try to get public IP (uses local IP for comparison) wan_ip=$(ifconfig 2>/dev/null | grep -E "inet addr:[0-9]" | grep -v "127.0.0.1" | head -1 | sed 's/.*inet addr:\([0-9.]*\).*/\1/') fi echo "$wan_ip" } # Helper: Get public IPv4 address (from external service) get_public_ipv4() { local ip="" # Try multiple services for reliability ip=$(wget -qO- -T 5 "http://ipv4.icanhazip.com" 2>/dev/null | tr -d '\n') [ -z "$ip" ] && ip=$(wget -qO- -T 5 "http://api.ipify.org" 2>/dev/null | tr -d '\n') [ -z "$ip" ] && ip=$(wget -qO- -T 5 "http://v4.ident.me" 2>/dev/null | tr -d '\n') echo "$ip" } # Helper: Get public IPv6 address (from external service) get_public_ipv6() { local ip="" # Try multiple services for reliability ip=$(wget -qO- -T 5 "http://ipv6.icanhazip.com" 2>/dev/null | tr -d '\n') [ -z "$ip" ] && ip=$(wget -qO- -T 5 "http://v6.ident.me" 2>/dev/null | tr -d '\n') echo "$ip" } # Helper: Check external port accessibility using portchecker service check_external_port() { local ip="$1" local port="$2" local result="" # Use canyouseeme.org API or similar # Try portquiz.net which echoes back on any port result=$(wget -qO- -T 5 "http://portquiz.net:${port}/" 2>/dev/null) if echo "$result" | grep -q "Port ${port}"; then return 0 fi # Alternative: try to connect to our own IP from outside perspective # Use online port checker API result=$(wget -qO- -T 8 "https://ports.yougetsignal.com/short-url-check-port.php?remoteAddress=${ip}&portNumber=${port}" 2>/dev/null) if echo "$result" | grep -qi "open"; then return 0 fi return 1 } # Helper: Reverse DNS lookup get_reverse_dns() { local ip="$1" local hostname="" if command -v nslookup >/dev/null 2>&1; then hostname=$(nslookup "$ip" 2>/dev/null | grep "name =" | head -1 | awk '{print $NF}' | sed 's/\.$//') elif command -v host >/dev/null 2>&1; then hostname=$(host "$ip" 2>/dev/null | grep "pointer" | head -1 | awk '{print $NF}' | sed 's/\.$//') fi echo "$hostname" } # Helper: Check certificate expiry check_cert_expiry() { local domain="$1" local cert_file="" # Try multiple possible certificate locations # 1. HAProxy certs directory (combined pem files) if [ -f "/srv/haproxy/certs/${domain}.pem" ]; then cert_file="/srv/haproxy/certs/${domain}.pem" # 2. ACME standard path elif [ -f "/etc/acme/${domain}/${domain}.cer" ]; then cert_file="/etc/acme/${domain}/${domain}.cer" # 3. ACME fullchain (some setups use this) elif [ -f "/etc/acme/${domain}/fullchain.cer" ]; then cert_file="/etc/acme/${domain}/fullchain.cer" # 4. ACME with _ecc suffix (ECC certs) elif [ -f "/etc/acme/${domain}_ecc/${domain}.cer" ]; then cert_file="/etc/acme/${domain}_ecc/${domain}.cer" # 5. Let's Encrypt standard path elif [ -f "/etc/letsencrypt/live/${domain}/cert.pem" ]; then cert_file="/etc/letsencrypt/live/${domain}/cert.pem" fi if [ -n "$cert_file" ] && [ -f "$cert_file" ]; then # Get expiry date using openssl local expiry_date expiry_date=$(openssl x509 -enddate -noout -in "$cert_file" 2>/dev/null | cut -d= -f2) if [ -n "$expiry_date" ]; then # Convert to epoch - try multiple date formats for compatibility local expiry_epoch now_epoch days_left # BusyBox date may not support -d with GMT format # Try direct parsing first expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null) # If that fails, try converting the format if [ -z "$expiry_epoch" ]; then # Parse "Apr 27 04:05:21 2026 GMT" format manually local month day time year month=$(echo "$expiry_date" | awk '{print $1}') day=$(echo "$expiry_date" | awk '{print $2}') time=$(echo "$expiry_date" | awk '{print $3}') year=$(echo "$expiry_date" | awk '{print $4}') # Convert month name to number case "$month" in Jan) month="01" ;; Feb) month="02" ;; Mar) month="03" ;; Apr) month="04" ;; May) month="05" ;; Jun) month="06" ;; Jul) month="07" ;; Aug) month="08" ;; Sep) month="09" ;; Oct) month="10" ;; Nov) month="11" ;; Dec) month="12" ;; esac # Try with reformatted date expiry_epoch=$(date -d "${year}-${month}-${day}" +%s 2>/dev/null) fi now_epoch=$(date +%s) if [ -n "$expiry_epoch" ] && [ -n "$now_epoch" ]; then days_left=$(( (expiry_epoch - now_epoch) / 86400 )) echo "$days_left" return 0 fi fi fi return 1 } # Helper: Check if external port is accessible (basic check via firewall rules) check_port_firewall_open() { local port="$1" local rule_name="" case "$port" in 80) rule_name="HAProxy-HTTP" ;; 443) rule_name="HAProxy-HTTPS" ;; esac # Check if firewall rule exists and is enabled local i=0 while uci -q get firewall.@rule[$i] >/dev/null 2>&1; do local name=$(uci -q get firewall.@rule[$i].name) local enabled=$(uci -q get firewall.@rule[$i].enabled) local dest_port=$(uci -q get firewall.@rule[$i].dest_port) if [ "$name" = "$rule_name" ] || [ "$dest_port" = "$port" ]; then if [ "$enabled" != "0" ]; then return 0 fi fi i=$((i + 1)) done return 1 } # Get network connectivity info (public IPs, port accessibility) # NOTE: External port checks disabled - too slow (HTTP requests to external services) method_get_network_info() { json_init json_add_boolean "success" 1 local lan_ip lan_ip=$(get_lan_ip) json_add_string "lan_ip" "$lan_ip" # Get public IPv4 (use uclient-fetch with IPv4 only for faster response) json_add_object "ipv4" local public_ipv4 public_ipv4=$(uclient-fetch -4 -q -T 2 -O - "http://ipv4.icanhazip.com" 2>/dev/null | tr -d '\n') if [ -n "$public_ipv4" ]; then json_add_string "address" "$public_ipv4" json_add_string "status" "ok" else json_add_string "status" "unavailable" fi json_close_object # IPv6 - skip network check (often broken/slow), just report firewall status json_add_object "ipv6" # Check if IPv6 is enabled in network config local ipv6_enabled=0 if uci -q get network.wan6 >/dev/null 2>&1 || \ uci -q get network.wan.ipv6 >/dev/null 2>&1; then ipv6_enabled=1 fi if [ "$ipv6_enabled" = "1" ]; then json_add_string "status" "configured" else json_add_string "status" "disabled" fi json_close_object # External port accessibility - use firewall check only (fast) # NOTE: Real external check would require slow HTTP requests to external services json_add_object "external_ports" local http_fw=0 local https_fw=0 check_port_firewall_open 80 && http_fw=1 check_port_firewall_open 443 && https_fw=1 json_add_object "http" if [ "$http_fw" = "1" ]; then json_add_string "status" "firewall_open" json_add_string "hint" "Firewall allows port 80" else json_add_string "status" "firewall_closed" json_add_string "hint" "Add firewall rule for port 80" fi json_close_object json_add_object "https" if [ "$https_fw" = "1" ]; then json_add_string "status" "firewall_open" json_add_string "hint" "Firewall allows port 443" else json_add_string "status" "firewall_closed" json_add_string "hint" "Add firewall rule for port 443" fi json_close_object json_close_object # Local firewall status json_add_object "firewall" json_add_boolean "http_open" "$http_fw" json_add_boolean "https_open" "$https_fw" if [ "$http_fw" = "1" ] && [ "$https_fw" = "1" ]; then json_add_string "status" "ok" elif [ "$http_fw" = "1" ] || [ "$https_fw" = "1" ]; then json_add_string "status" "partial" else json_add_string "status" "closed" fi json_close_object # HAProxy status json_add_object "haproxy" if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then json_add_string "status" "running" else json_add_string "status" "stopped" fi json_close_object json_dump } # Check health status for a service method_check_service_health() { local service_id domain read -r input json_load "$input" json_get_var service_id service_id json_get_var domain domain "" json_init if [ -z "$service_id" ] && [ -z "$domain" ]; then json_add_boolean "success" 0 json_add_string "error" "service_id or domain is required" json_dump return fi # If only service_id provided, get domain from config if [ -z "$domain" ] && [ -n "$service_id" ]; then config_load "$UCI_CONFIG" config_get domain "$service_id" haproxy_domain "" fi json_add_boolean "success" 1 json_add_string "service_id" "$service_id" json_add_string "domain" "$domain" # Get public IPv4 (short timeout for responsiveness) local public_ipv4 public_ipv4=$(wget -qO- -T 3 "http://ipv4.icanhazip.com" 2>/dev/null | tr -d '\n') # Public IP info json_add_object "public_ip" json_add_string "ipv4" "${public_ipv4:-unknown}" json_close_object # DNS check with public IP comparison json_add_object "dns" if [ -n "$domain" ]; then local resolved_ip resolved_ip=$(check_dns_resolution "$domain") if [ -n "$resolved_ip" ]; then json_add_string "resolved_ip" "$resolved_ip" # Check if DNS points to public IP or private IP case "$resolved_ip" in 10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.2*|172.30.*|172.31.*|192.168.*) json_add_string "status" "private" json_add_string "error" "DNS points to private IP (not reachable from internet)" json_add_string "expected" "$public_ipv4" ;; *) if [ "$resolved_ip" = "$public_ipv4" ]; then json_add_string "status" "ok" else json_add_string "status" "mismatch" json_add_string "expected" "$public_ipv4" json_add_string "hint" "DNS points to different IP than your public IP" fi ;; esac else json_add_string "status" "failed" json_add_string "error" "DNS resolution failed" fi else json_add_string "status" "none" fi json_close_object # External port accessibility check (firewall-based, fast) json_add_object "external_access" local http_fw=0 local https_fw=0 check_port_firewall_open 80 && http_fw=1 check_port_firewall_open 443 && https_fw=1 json_add_boolean "http_accessible" "$http_fw" json_add_boolean "https_accessible" "$https_fw" if [ "$http_fw" = "1" ] && [ "$https_fw" = "1" ]; then json_add_string "status" "firewall_ok" elif [ "$http_fw" = "1" ] || [ "$https_fw" = "1" ]; then json_add_string "status" "partial" else json_add_string "status" "closed" json_add_string "hint" "Open firewall ports 80/443 for external access" fi json_close_object # Certificate check json_add_object "certificate" if [ -n "$domain" ]; then local days_left days_left=$(check_cert_expiry "$domain") if [ -n "$days_left" ]; then if [ "$days_left" -lt 0 ]; then json_add_string "status" "expired" elif [ "$days_left" -lt 7 ]; then json_add_string "status" "critical" elif [ "$days_left" -lt 30 ]; then json_add_string "status" "warning" else json_add_string "status" "ok" fi json_add_int "days_left" "$days_left" else json_add_string "status" "missing" fi else json_add_string "status" "none" fi json_close_object # Port/Firewall check json_add_object "firewall" local http_open=0 local https_open=0 check_port_firewall_open 80 && http_open=1 check_port_firewall_open 443 && https_open=1 if [ "$http_open" = "1" ] && [ "$https_open" = "1" ]; then json_add_string "status" "ok" elif [ "$http_open" = "1" ] || [ "$https_open" = "1" ]; then json_add_string "status" "partial" else json_add_string "status" "closed" fi json_add_boolean "http_open" "$http_open" json_add_boolean "https_open" "$https_open" json_close_object # HAProxy status json_add_object "haproxy" if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then json_add_string "status" "running" else json_add_string "status" "stopped" fi json_close_object json_dump } # Batch health check for all published services (for dashboard) method_check_all_health() { json_init json_add_object "health" local wan_ip wan_ip=$(get_wan_ip) json_add_string "wan_ip" "$wan_ip" # Check HAProxy status json_add_object "haproxy" if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then json_add_string "status" "running" else json_add_string "status" "stopped" fi json_close_object # Check Tor status json_add_object "tor" if pgrep -f "/usr/sbin/tor" >/dev/null 2>&1; then json_add_string "status" "running" else json_add_string "status" "stopped" fi json_close_object # Check firewall ports json_add_object "firewall" local http_open=0 local https_open=0 check_port_firewall_open 80 && http_open=1 check_port_firewall_open 443 && https_open=1 json_add_boolean "http_open" "$http_open" json_add_boolean "https_open" "$https_open" if [ "$http_open" = "1" ] && [ "$https_open" = "1" ]; then json_add_string "status" "ok" elif [ "$http_open" = "1" ] || [ "$https_open" = "1" ]; then json_add_string "status" "partial" else json_add_string "status" "closed" fi json_close_object # Check individual services with domains json_add_array "services" # Get all published services with domains from HAProxy (if available) local vhosts_json if haproxy_available; then vhosts_json=$(ubus call luci.haproxy list_vhosts 2>/dev/null) fi if [ -n "$vhosts_json" ]; then local count count=$(echo "$vhosts_json" | jsonfilter -e '@.vhosts[*].domain' 2>/dev/null | wc -l) local i=0 while [ $i -lt "$count" ]; do local domain enabled domain=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].domain" 2>/dev/null) enabled=$(echo "$vhosts_json" | jsonfilter -e "@.vhosts[$i].enabled" 2>/dev/null) i=$((i + 1)) [ -z "$domain" ] && continue [ "$enabled" = "false" ] || [ "$enabled" = "0" ] && continue json_add_object json_add_string "domain" "$domain" # DNS check local resolved_ip resolved_ip=$(check_dns_resolution "$domain") if [ -n "$resolved_ip" ]; then json_add_string "dns_status" "ok" json_add_string "dns_ip" "$resolved_ip" else json_add_string "dns_status" "failed" fi # Cert check local days_left days_left=$(check_cert_expiry "$domain") if [ -n "$days_left" ]; then if [ "$days_left" -lt 0 ]; then json_add_string "cert_status" "expired" elif [ "$days_left" -lt 7 ]; then json_add_string "cert_status" "critical" elif [ "$days_left" -lt 30 ]; then json_add_string "cert_status" "warning" else json_add_string "cert_status" "ok" fi json_add_int "cert_days" "$days_left" else json_add_string "cert_status" "missing" fi json_close_object done fi json_close_array json_close_object json_dump } # Get certificate status for service method_get_certificate_status() { local service_id read -r input json_load "$input" json_get_var service_id service_id json_init if [ -z "$service_id" ]; then json_add_boolean "success" 0 json_add_string "error" "service_id is required" json_dump return fi config_load "$UCI_CONFIG" local haproxy_domain config_get haproxy_domain "$service_id" haproxy_domain "" if [ -z "$haproxy_domain" ]; then json_add_boolean "success" 0 json_add_string "error" "No domain configured" json_dump return fi # Get certificate expiry info local days_left days_left=$(check_cert_expiry "$haproxy_domain") json_add_boolean "success" 1 json_add_string "domain" "$haproxy_domain" if [ -n "$days_left" ]; then if [ "$days_left" -lt 0 ]; then json_add_string "status" "expired" elif [ "$days_left" -lt 7 ]; then json_add_string "status" "critical" elif [ "$days_left" -lt 30 ]; then json_add_string "status" "warning" else json_add_string "status" "valid" fi json_add_int "days_left" "$days_left" else json_add_string "status" "missing" fi json_dump } # Get landing page config method_get_landing_config() { json_init config_load "$UCI_CONFIG" local landing_path auto_regen theme config_get landing_path main landing_path "/www/secubox-services.html" config_get auto_regen main landing_auto_regen "1" config_get theme main landing_theme "mirrorbox" json_add_string "path" "$landing_path" json_add_boolean "auto_regen" "$auto_regen" json_add_string "theme" "$theme" # Check if file exists if [ -f "$landing_path" ]; then json_add_boolean "exists" 1 local mtime mtime=$(stat -c %Y "$landing_path" 2>/dev/null) json_add_int "modified" "${mtime:-0}" else json_add_boolean "exists" 0 fi json_dump } # Save landing page config method_save_landing_config() { local auto_regen read -r input json_load "$input" json_get_var auto_regen auto_regen "" json_init [ -n "$auto_regen" ] && uci set "$UCI_CONFIG.main.landing_auto_regen=$auto_regen" uci commit "$UCI_CONFIG" json_add_boolean "success" 1 json_dump } # Set landing page theme method_set_landing_theme() { local theme read -r input json_load "$input" json_get_var theme theme "mirrorbox" json_init # Validate theme case "$theme" in mirrorbox|cyberpunk|minimal|terminal|light) uci set "$UCI_CONFIG.main.landing_theme=$theme" uci commit "$UCI_CONFIG" json_add_boolean "success" 1 json_add_string "theme" "$theme" ;; *) json_add_boolean "success" 0 json_add_string "error" "Invalid theme: $theme" ;; esac json_dump } # Main RPC interface case "$1" in list) cat <<'EOF' { "list_services": {}, "get_service": { "service_id": "string" }, "publish_service": { "name": "string", "local_port": "integer", "domain": "string", "tor_enabled": "boolean", "category": "string", "icon": "string" }, "unpublish_service": { "service_id": "string" }, "update_service": { "service_id": "string", "name": "string", "category": "string", "icon": "string" }, "delete_service": { "service_id": "string" }, "sync_providers": {}, "generate_landing_page": {}, "get_qr_data": { "service_id": "string", "url_type": "string" }, "list_categories": {}, "get_certificate_status": { "service_id": "string" }, "check_service_health": { "service_id": "string", "domain": "string" }, "check_all_health": {}, "get_network_info": {}, "get_landing_config": {}, "save_landing_config": { "auto_regen": "boolean" }, "set_landing_theme": { "theme": "string" } } EOF ;; call) case "$2" in list_services) method_list_services ;; get_service) method_get_service ;; publish_service) method_publish_service ;; unpublish_service) method_unpublish_service ;; update_service) method_update_service ;; delete_service) method_delete_service ;; sync_providers) method_sync_providers ;; generate_landing_page) method_generate_landing_page ;; get_qr_data) method_get_qr_data ;; list_categories) method_list_categories ;; get_certificate_status) method_get_certificate_status ;; check_service_health) method_check_service_health ;; check_all_health) method_check_all_health ;; get_network_info) method_get_network_info ;; get_landing_config) method_get_landing_config ;; save_landing_config) method_save_landing_config ;; set_landing_theme) method_set_landing_theme ;; *) json_init json_add_boolean "error" 1 json_add_string "message" "Unknown method: $2" json_dump ;; esac ;; esac