- metablogizer: Use network.lan.ipaddr instead of 127.0.0.1 for server address - service-registry: Same fix for emancipate function - hexojs: Same fix for HAProxy backend creation - gotosocial: Switch from LXC to direct execution mode - v0.18.0 has cgroup bugs, using v0.17.0 instead - Remove LXC container dependency - Use /srv/gotosocial for binary and data - Add proper PID file management The HAProxy container cannot reach 127.0.0.1 on the host, so all HAProxy backend servers must use the LAN IP (typically 192.168.255.1). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1641 lines
45 KiB
Bash
Executable File
1641 lines
45 KiB
Bash
Executable File
#!/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
|