#!/bin/sh # SPDX-License-Identifier: MIT # LuCI RPCD backend for HAProxy # Copyright (C) 2025 CyberMind.fr . /lib/functions.sh . /usr/share/libubox/jshn.sh HAPROXYCTL="/usr/sbin/haproxyctl" UCI_CONFIG="haproxy" # Helper: Run haproxyctl command run_ctl() { if [ -x "$HAPROXYCTL" ]; then "$HAPROXYCTL" "$@" 2>&1 else echo "haproxyctl not found" return 1 fi } # 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: Set UCI value set_uci() { local section="$1" local option="$2" local value="$3" uci set "$UCI_CONFIG.$section.$option=$value" } # Helper: List UCI sections of type list_sections() { local type="$1" uci -q show "$UCI_CONFIG" | grep "=$type\$" | cut -d. -f2 | cut -d= -f1 } # Status method method_status() { local enabled http_port https_port stats_port stats_enabled local container_running haproxy_running enabled=$(get_uci main enabled 0) http_port=$(get_uci main http_port 80) https_port=$(get_uci main https_port 443) stats_port=$(get_uci main stats_port 8404) stats_enabled=$(get_uci main stats_enabled 1) # Check container status - Docker first, then LXC container_running="0" haproxy_running="0" # Check Docker container (secubox-haproxy or haproxy) if command -v docker >/dev/null 2>&1; then if docker ps --format '{{.Names}}' 2>/dev/null | grep -qE '^(secubox-haproxy|haproxy)$'; then container_running="1" haproxy_running="1" fi fi # If not Docker, check LXC if [ "$container_running" = "0" ]; then if command -v lxc-info >/dev/null 2>&1; then if lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING"; then container_running="1" # Check HAProxy process inside LXC haproxy_running=$(lxc-attach -n haproxy -- pgrep haproxy >/dev/null 2>&1 && echo "1" || echo "0") fi fi fi # Final fallback: check if HAProxy port is listening if [ "$container_running" = "0" ]; then if netstat -tln 2>/dev/null | grep -q ":80 "; then container_running="1" haproxy_running="1" fi fi json_init json_add_boolean "enabled" "$enabled" json_add_int "http_port" "$http_port" json_add_int "https_port" "$https_port" json_add_int "stats_port" "$stats_port" json_add_boolean "stats_enabled" "$stats_enabled" json_add_boolean "container_running" "$container_running" json_add_boolean "haproxy_running" "$haproxy_running" json_dump } # Get stats method_get_stats() { local stats_output container_running # Check if container is running (use same detection as status method) if command -v lxc-info >/dev/null 2>&1; then container_running=$(lxc-info -n haproxy -s 2>/dev/null | grep -q "RUNNING" && echo "1" || echo "0") else container_running=$(pgrep -f "lxc-start.*-n haproxy" >/dev/null 2>&1 && echo "1" || echo "0") fi if [ "$container_running" = "1" ]; then # Get stats via HAProxy socket stats_output=$(run_ctl stats 2>/dev/null) if [ -n "$stats_output" ]; then json_init json_add_boolean "success" 1 json_add_string "stats" "$stats_output" json_dump return fi fi json_init json_add_boolean "success" 0 json_add_string "error" "HAProxy not running or stats unavailable" json_dump } # List vhosts method_list_vhosts() { json_init json_add_array "vhosts" config_load "$UCI_CONFIG" config_foreach _add_vhost vhost json_close_array json_dump } _add_vhost() { local section="$1" local domain backend ssl ssl_redirect acme enabled config_get domain "$section" domain "" config_get backend "$section" backend "" config_get ssl "$section" ssl "0" config_get ssl_redirect "$section" ssl_redirect "1" config_get acme "$section" acme "0" config_get enabled "$section" enabled "1" json_add_object json_add_string "id" "$section" json_add_string "domain" "$domain" json_add_string "backend" "$backend" json_add_boolean "ssl" "$ssl" json_add_boolean "ssl_redirect" "$ssl_redirect" json_add_boolean "acme" "$acme" json_add_boolean "enabled" "$enabled" json_close_object } # Get vhost method_get_vhost() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing vhost id" json_dump return fi local domain backend ssl ssl_redirect acme enabled domain=$(get_uci "$id" domain "") backend=$(get_uci "$id" backend "") ssl=$(get_uci "$id" ssl "0") ssl_redirect=$(get_uci "$id" ssl_redirect "1") acme=$(get_uci "$id" acme "0") enabled=$(get_uci "$id" enabled "1") json_init json_add_boolean "success" 1 json_add_string "id" "$id" json_add_string "domain" "$domain" json_add_string "backend" "$backend" json_add_boolean "ssl" "$ssl" json_add_boolean "ssl_redirect" "$ssl_redirect" json_add_boolean "acme" "$acme" json_add_boolean "enabled" "$enabled" json_dump } # Create vhost method_create_vhost() { local domain backend ssl ssl_redirect acme enabled local section_id read -r input json_load "$input" json_get_var domain domain json_get_var backend backend json_get_var ssl ssl "0" json_get_var ssl_redirect ssl_redirect "1" json_get_var acme acme "0" json_get_var enabled enabled "1" if [ -z "$domain" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Domain is required" json_dump return fi # Generate section ID from domain section_id=$(echo "$domain" | sed 's/[^a-zA-Z0-9]/_/g') uci set "$UCI_CONFIG.$section_id=vhost" uci set "$UCI_CONFIG.$section_id.domain=$domain" uci set "$UCI_CONFIG.$section_id.backend=$backend" uci set "$UCI_CONFIG.$section_id.ssl=$ssl" uci set "$UCI_CONFIG.$section_id.ssl_redirect=$ssl_redirect" uci set "$UCI_CONFIG.$section_id.acme=$acme" uci set "$UCI_CONFIG.$section_id.enabled=$enabled" uci commit "$UCI_CONFIG" # Regenerate HAProxy config run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_add_string "id" "$section_id" json_dump } # Update vhost method_update_vhost() { local id domain backend ssl ssl_redirect acme enabled read -r input json_load "$input" json_get_var id id json_get_var domain domain json_get_var backend backend json_get_var ssl ssl json_get_var ssl_redirect ssl_redirect json_get_var acme acme json_get_var enabled enabled if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing vhost id" json_dump return fi [ -n "$domain" ] && uci set "$UCI_CONFIG.$id.domain=$domain" [ -n "$backend" ] && uci set "$UCI_CONFIG.$id.backend=$backend" [ -n "$ssl" ] && uci set "$UCI_CONFIG.$id.ssl=$ssl" [ -n "$ssl_redirect" ] && uci set "$UCI_CONFIG.$id.ssl_redirect=$ssl_redirect" [ -n "$acme" ] && uci set "$UCI_CONFIG.$id.acme=$acme" [ -n "$enabled" ] && uci set "$UCI_CONFIG.$id.enabled=$enabled" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Delete vhost method_delete_vhost() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing vhost id" json_dump return fi uci delete "$UCI_CONFIG.$id" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # List backends method_list_backends() { json_init json_add_array "backends" config_load "$UCI_CONFIG" config_foreach _add_backend backend json_close_array json_dump } _add_backend() { local section="$1" local name mode balance health_check health_check_uri enabled server_line config_get name "$section" name "$section" config_get mode "$section" mode "http" config_get balance "$section" balance "roundrobin" config_get health_check "$section" health_check "" config_get health_check_uri "$section" health_check_uri "" config_get enabled "$section" enabled "1" config_get server_line "$section" server "" json_add_object json_add_string "id" "$section" json_add_string "name" "$name" json_add_string "mode" "$mode" json_add_string "balance" "$balance" json_add_string "health_check" "$health_check" json_add_string "health_check_uri" "$health_check_uri" json_add_boolean "enabled" "$enabled" # Include servers array - parse inline server option if present json_add_array "servers" if [ -n "$server_line" ]; then # Parse inline format: "name address:port [options]" local srv_name srv_addr_port srv_addr srv_port srv_check srv_name=$(echo "$server_line" | awk '{print $1}') srv_addr_port=$(echo "$server_line" | awk '{print $2}') srv_addr=$(echo "$srv_addr_port" | cut -d: -f1) srv_port=$(echo "$srv_addr_port" | cut -d: -f2) srv_check=$(echo "$server_line" | grep -q "check" && echo "1" || echo "0") json_add_object json_add_string "id" "${section}_${srv_name}" json_add_string "name" "$srv_name" json_add_string "address" "$srv_addr" json_add_int "port" "${srv_port:-80}" json_add_int "weight" "100" json_add_boolean "check" "$srv_check" json_add_boolean "enabled" "1" json_add_boolean "inline" "1" json_close_object fi # Also check for separate server sections config_foreach _add_server_for_backend_inline server "$section" json_close_array json_close_object } _add_server_for_backend_inline() { local srv_section="$1" local backend_filter="$2" local backend srv_name srv_address srv_port srv_weight srv_check srv_enabled config_get backend "$srv_section" backend "" [ "$backend" != "$backend_filter" ] && return config_get srv_name "$srv_section" name "$srv_section" config_get srv_address "$srv_section" address "" config_get srv_port "$srv_section" port "" config_get srv_weight "$srv_section" weight "100" config_get srv_check "$srv_section" check "1" config_get srv_enabled "$srv_section" enabled "1" json_add_object json_add_string "id" "$srv_section" json_add_string "name" "$srv_name" json_add_string "address" "$srv_address" json_add_int "port" "${srv_port:-80}" json_add_int "weight" "$srv_weight" json_add_boolean "check" "$srv_check" json_add_boolean "enabled" "$srv_enabled" json_close_object } # Get backend method_get_backend() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing backend id" json_dump return fi local name mode balance health_check enabled name=$(get_uci "$id" name "$id") mode=$(get_uci "$id" mode "http") balance=$(get_uci "$id" balance "roundrobin") health_check=$(get_uci "$id" health_check "") enabled=$(get_uci "$id" enabled "1") json_init json_add_boolean "success" 1 json_add_string "id" "$id" json_add_string "name" "$name" json_add_string "mode" "$mode" json_add_string "balance" "$balance" json_add_string "health_check" "$health_check" json_add_boolean "enabled" "$enabled" # Add servers for this backend json_add_array "servers" config_load "$UCI_CONFIG" config_foreach _add_server_for_backend server "$id" json_close_array json_dump } _add_server_for_backend() { local section="$1" local backend_filter="$2" local backend name address port weight check enabled config_get backend "$section" backend "" [ "$backend" != "$backend_filter" ] && return config_get name "$section" name "$section" config_get address "$section" address "" config_get port "$section" port "" config_get weight "$section" weight "100" config_get check "$section" check "1" config_get enabled "$section" enabled "1" json_add_object json_add_string "id" "$section" json_add_string "name" "$name" json_add_string "address" "$address" json_add_int "port" "$port" json_add_int "weight" "$weight" json_add_boolean "check" "$check" json_add_boolean "enabled" "$enabled" json_close_object } # Create backend method_create_backend() { local name mode balance health_check health_check_uri enabled local section_id read -r input json_load "$input" json_get_var name name json_get_var mode mode "http" json_get_var balance balance "roundrobin" json_get_var health_check health_check "" json_get_var health_check_uri health_check_uri "" json_get_var enabled enabled "1" if [ -z "$name" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Backend name is required" json_dump return fi section_id=$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g') uci set "$UCI_CONFIG.$section_id=backend" uci set "$UCI_CONFIG.$section_id.name=$name" uci set "$UCI_CONFIG.$section_id.mode=$mode" uci set "$UCI_CONFIG.$section_id.balance=$balance" [ -n "$health_check" ] && uci set "$UCI_CONFIG.$section_id.health_check=$health_check" [ -n "$health_check_uri" ] && uci set "$UCI_CONFIG.$section_id.health_check_uri=$health_check_uri" uci set "$UCI_CONFIG.$section_id.enabled=$enabled" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_add_string "id" "$section_id" json_dump } # Update backend method_update_backend() { local id name mode balance health_check health_check_uri enabled read -r input json_load "$input" json_get_var id id json_get_var name name json_get_var mode mode json_get_var balance balance json_get_var health_check health_check json_get_var health_check_uri health_check_uri json_get_var enabled enabled if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing backend id" json_dump return fi [ -n "$name" ] && uci set "$UCI_CONFIG.$id.name=$name" [ -n "$mode" ] && uci set "$UCI_CONFIG.$id.mode=$mode" [ -n "$balance" ] && uci set "$UCI_CONFIG.$id.balance=$balance" [ -n "$health_check" ] && uci set "$UCI_CONFIG.$id.health_check=$health_check" [ -n "$health_check_uri" ] && uci set "$UCI_CONFIG.$id.health_check_uri=$health_check_uri" [ -n "$enabled" ] && uci set "$UCI_CONFIG.$id.enabled=$enabled" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Delete backend method_delete_backend() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing backend id" json_dump return fi # Delete associated servers config_load "$UCI_CONFIG" config_foreach _delete_server_for_backend server "$id" uci delete "$UCI_CONFIG.$id" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } _delete_server_for_backend() { local section="$1" local backend_filter="$2" local backend config_get backend "$section" backend "" [ "$backend" = "$backend_filter" ] && uci delete "$UCI_CONFIG.$section" } # List servers method_list_servers() { local backend_filter read -r input json_load "$input" json_get_var backend_filter backend "" json_init json_add_array "servers" config_load "$UCI_CONFIG" if [ -n "$backend_filter" ]; then config_foreach _add_server_for_backend server "$backend_filter" else config_foreach _add_server server fi json_close_array json_dump } _add_server() { local section="$1" local backend name address port weight check enabled config_get backend "$section" backend "" config_get name "$section" name "$section" config_get address "$section" address "" config_get port "$section" port "" config_get weight "$section" weight "100" config_get check "$section" check "1" config_get enabled "$section" enabled "1" json_add_object json_add_string "id" "$section" json_add_string "backend" "$backend" json_add_string "name" "$name" json_add_string "address" "$address" json_add_int "port" "$port" json_add_int "weight" "$weight" json_add_boolean "check" "$check" json_add_boolean "enabled" "$enabled" json_close_object } # Create server method_create_server() { local backend name address port weight check enabled local section_id read -r input json_load "$input" json_get_var backend backend json_get_var name name json_get_var address address json_get_var port port json_get_var weight weight "100" json_get_var check check "1" json_get_var enabled enabled "1" if [ -z "$backend" ] || [ -z "$name" ] || [ -z "$address" ] || [ -z "$port" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Backend, name, address and port are required" json_dump return fi section_id="${backend}_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')" uci set "$UCI_CONFIG.$section_id=server" uci set "$UCI_CONFIG.$section_id.backend=$backend" uci set "$UCI_CONFIG.$section_id.name=$name" uci set "$UCI_CONFIG.$section_id.address=$address" uci set "$UCI_CONFIG.$section_id.port=$port" uci set "$UCI_CONFIG.$section_id.weight=$weight" uci set "$UCI_CONFIG.$section_id.check=$check" uci set "$UCI_CONFIG.$section_id.enabled=$enabled" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_add_string "id" "$section_id" json_dump } # Update server method_update_server() { local id backend name address port weight check enabled inline read -r input json_load "$input" json_get_var id id json_get_var backend backend json_get_var name name json_get_var address address json_get_var port port json_get_var weight weight json_get_var check check json_get_var enabled enabled json_get_var inline inline "" if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing server id" json_dump return fi # Check if this is an inline server (id format: backendname_servername) # If so, we need to convert it to a proper server section if [ "$inline" = "1" ] || ! uci -q get "$UCI_CONFIG.$id" >/dev/null 2>&1; then # This is an inline server - extract backend from id local backend_id backend_id=$(echo "$id" | sed 's/_[^_]*$//') # Remove inline server option from backend uci -q delete "$UCI_CONFIG.$backend_id.server" # Create new server section local section_id="${backend_id}_${name}" uci set "$UCI_CONFIG.$section_id=server" uci set "$UCI_CONFIG.$section_id.backend=$backend_id" uci set "$UCI_CONFIG.$section_id.name=$name" uci set "$UCI_CONFIG.$section_id.address=$address" uci set "$UCI_CONFIG.$section_id.port=$port" [ -n "$weight" ] && uci set "$UCI_CONFIG.$section_id.weight=$weight" [ -n "$check" ] && uci set "$UCI_CONFIG.$section_id.check=$check" [ -n "$enabled" ] && uci set "$UCI_CONFIG.$section_id.enabled=$enabled" else # Regular server section - update in place [ -n "$backend" ] && uci set "$UCI_CONFIG.$id.backend=$backend" [ -n "$name" ] && uci set "$UCI_CONFIG.$id.name=$name" [ -n "$address" ] && uci set "$UCI_CONFIG.$id.address=$address" [ -n "$port" ] && uci set "$UCI_CONFIG.$id.port=$port" [ -n "$weight" ] && uci set "$UCI_CONFIG.$id.weight=$weight" [ -n "$check" ] && uci set "$UCI_CONFIG.$id.check=$check" [ -n "$enabled" ] && uci set "$UCI_CONFIG.$id.enabled=$enabled" fi uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Delete server method_delete_server() { local id inline read -r input json_load "$input" json_get_var id id json_get_var inline inline "" if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing server id" json_dump return fi # Check if this is an inline server or regular server section if [ "$inline" = "1" ] || ! uci -q get "$UCI_CONFIG.$id" >/dev/null 2>&1; then # Inline server - extract backend id and delete the server option local backend_id backend_id=$(echo "$id" | sed 's/_[^_]*$//') uci -q delete "$UCI_CONFIG.$backend_id.server" else # Regular server section uci delete "$UCI_CONFIG.$id" fi uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # List certificates method_list_certificates() { json_init json_add_array "certificates" config_load "$UCI_CONFIG" config_foreach _add_certificate certificate json_close_array json_dump } _add_certificate() { local section="$1" local domain type enabled config_get domain "$section" domain "" config_get type "$section" type "acme" config_get enabled "$section" enabled "1" json_add_object json_add_string "id" "$section" json_add_string "domain" "$domain" json_add_string "type" "$type" json_add_boolean "enabled" "$enabled" json_close_object } # Async certificate request task directory CERT_TASK_DIR="/tmp/haproxy-cert-tasks" mkdir -p "$CERT_TASK_DIR" 2>/dev/null # Start async certificate request (returns immediately with task_id) method_start_cert_request() { local domain staging read -r input json_load "$input" json_get_var domain domain json_get_var staging staging if [ -z "$domain" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Domain is required" json_dump return fi # Generate task ID local task_id task_id="cert_$(date +%s)_$$" local task_file="$CERT_TASK_DIR/$task_id" # Validate domain format if ! echo "$domain" | grep -qE '^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$'; then json_init json_add_boolean "success" 0 json_add_string "error" "Invalid domain format" json_dump return fi # Initialize task status cat > "$task_file" </dev/null 2>&1; then sed -i 's/"status": "[^"]*"/"status": "failed"/' "$task_file" sed -i 's/"message": "[^"]*"/"message": "DNS lookup failed for domain"/' "$task_file" sed -i "s/\"updated\": [0-9]*/\"updated\": $(date +%s)/" "$task_file" exit 1 fi # Update status: requesting sed -i 's/"phase": "[^"]*"/"phase": "requesting"/' "$task_file" sed -i 's/"message": "[^"]*"/"message": "Requesting certificate from ACME..."/' "$task_file" sed -i "s/\"updated\": [0-9]*/\"updated\": $(date +%s)/" "$task_file" # Run certificate request local acme_result acme_rc if [ "${staging:-0}" = "1" ]; then acme_result=$("$HAPROXYCTL" cert add "$domain" --staging 2>&1) else acme_result=$("$HAPROXYCTL" cert add "$domain" 2>&1) fi acme_rc=$? # Update status based on result if [ $acme_rc -eq 0 ]; then # Verify certificate was created sed -i 's/"phase": "[^"]*"/"phase": "verifying"/' "$task_file" sed -i 's/"message": "[^"]*"/"message": "Verifying certificate..."/' "$task_file" sed -i "s/\"updated\": [0-9]*/\"updated\": $(date +%s)/" "$task_file" sleep 1 # Check if cert file exists local cert_file="/srv/haproxy/certs/${domain}.pem" if [ -f "$cert_file" ]; then local expiry issuer expiry=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | sed 's/notAfter=//') issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | sed 's/.*O = //' | cut -d',' -f1) sed -i 's/"status": "[^"]*"/"status": "success"/' "$task_file" sed -i 's/"phase": "[^"]*"/"phase": "complete"/' "$task_file" sed -i "s/\"message\": \"[^\"]*\"/\"message\": \"Certificate issued by $issuer, expires $expiry\"/" "$task_file" sed -i "s/\"updated\": [0-9]*/\"updated\": $(date +%s)/" "$task_file" else sed -i 's/"status": "[^"]*"/"status": "failed"/' "$task_file" sed -i "s/\"message\": \"[^\"]*\"/\"message\": \"Certificate file not found after request\"/" "$task_file" sed -i "s/\"updated\": [0-9]*/\"updated\": $(date +%s)/" "$task_file" fi else sed -i 's/"status": "[^"]*"/"status": "failed"/' "$task_file" # Escape special chars in error message local safe_error safe_error=$(echo "$acme_result" | tr '\n' ' ' | sed 's/"/\\"/g' | cut -c1-200) sed -i "s/\"message\": \"[^\"]*\"/\"message\": \"$safe_error\"/" "$task_file" sed -i "s/\"updated\": [0-9]*/\"updated\": $(date +%s)/" "$task_file" fi ) & # Return task ID immediately json_init json_add_boolean "success" 1 json_add_string "task_id" "$task_id" json_add_string "message" "Certificate request started" json_dump } # Get certificate task status method_get_cert_task() { local task_id read -r input json_load "$input" json_get_var task_id task_id if [ -z "$task_id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "task_id is required" json_dump return fi local task_file="$CERT_TASK_DIR/$task_id" if [ ! -f "$task_file" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Task not found" json_dump return fi # Return task file contents cat "$task_file" } # List all certificate tasks method_list_cert_tasks() { json_init json_add_array "tasks" for task_file in "$CERT_TASK_DIR"/cert_*; do [ -f "$task_file" ] || continue local task_id status domain phase task_id=$(basename "$task_file") status=$(jsonfilter -i "$task_file" -e '@.status' 2>/dev/null) domain=$(jsonfilter -i "$task_file" -e '@.domain' 2>/dev/null) phase=$(jsonfilter -i "$task_file" -e '@.phase' 2>/dev/null) json_add_object "" json_add_string "task_id" "$task_id" json_add_string "domain" "$domain" json_add_string "status" "$status" json_add_string "phase" "$phase" json_close_object done json_close_array json_dump } # Clean old certificate tasks (> 1 hour) method_clean_cert_tasks() { local cleaned=0 local now now=$(date +%s) for task_file in "$CERT_TASK_DIR"/cert_*; do [ -f "$task_file" ] || continue local started started=$(jsonfilter -i "$task_file" -e '@.started' 2>/dev/null) if [ -n "$started" ] && [ $((now - started)) -gt 3600 ]; then rm -f "$task_file" cleaned=$((cleaned + 1)) fi done json_init json_add_boolean "success" 1 json_add_int "cleaned" "$cleaned" json_dump } # Request certificate (ACME) - synchronous (kept for compatibility) method_request_certificate() { local domain read -r input json_load "$input" json_get_var domain domain if [ -z "$domain" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Domain is required" json_dump return fi local result result=$(run_ctl cert add "$domain" 2>&1) local rc=$? json_init if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "Certificate requested for $domain" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump } # Import certificate method_import_certificate() { local domain cert_data key_data read -r input json_load "$input" json_get_var domain domain json_get_var cert_data cert json_get_var key_data key if [ -z "$domain" ] || [ -z "$cert_data" ] || [ -z "$key_data" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Domain, certificate and key are required" json_dump return fi local result result=$(run_ctl cert import "$domain" "$cert_data" "$key_data" 2>&1) local rc=$? json_init if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "Certificate imported for $domain" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump } # Delete certificate method_delete_certificate() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing certificate id" json_dump return fi local domain domain=$(get_uci "$id" domain "") # Remove certificate files run_ctl cert remove "$domain" >/dev/null 2>&1 uci delete "$UCI_CONFIG.$id" uci commit "$UCI_CONFIG" json_init json_add_boolean "success" 1 json_dump } # List ACLs method_list_acls() { json_init json_add_array "acls" config_load "$UCI_CONFIG" config_foreach _add_acl acl json_close_array json_dump } _add_acl() { local section="$1" local name type pattern backend enabled config_get name "$section" name "$section" config_get type "$section" type "" config_get pattern "$section" pattern "" config_get backend "$section" backend "" config_get enabled "$section" enabled "1" json_add_object json_add_string "id" "$section" json_add_string "name" "$name" json_add_string "type" "$type" json_add_string "pattern" "$pattern" json_add_string "backend" "$backend" json_add_boolean "enabled" "$enabled" json_close_object } # Create ACL method_create_acl() { local name type pattern backend enabled local section_id read -r input json_load "$input" json_get_var name name json_get_var type type json_get_var pattern pattern json_get_var backend backend json_get_var enabled enabled "1" if [ -z "$name" ] || [ -z "$type" ] || [ -z "$pattern" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Name, type and pattern are required" json_dump return fi section_id="acl_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')" uci set "$UCI_CONFIG.$section_id=acl" uci set "$UCI_CONFIG.$section_id.name=$name" uci set "$UCI_CONFIG.$section_id.type=$type" uci set "$UCI_CONFIG.$section_id.pattern=$pattern" [ -n "$backend" ] && uci set "$UCI_CONFIG.$section_id.backend=$backend" uci set "$UCI_CONFIG.$section_id.enabled=$enabled" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_add_string "id" "$section_id" json_dump } # Update ACL method_update_acl() { local id name type pattern backend enabled read -r input json_load "$input" json_get_var id id json_get_var name name json_get_var type type json_get_var pattern pattern json_get_var backend backend json_get_var enabled enabled if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing ACL id" json_dump return fi [ -n "$name" ] && uci set "$UCI_CONFIG.$id.name=$name" [ -n "$type" ] && uci set "$UCI_CONFIG.$id.type=$type" [ -n "$pattern" ] && uci set "$UCI_CONFIG.$id.pattern=$pattern" [ -n "$backend" ] && uci set "$UCI_CONFIG.$id.backend=$backend" [ -n "$enabled" ] && uci set "$UCI_CONFIG.$id.enabled=$enabled" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Delete ACL method_delete_acl() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing ACL id" json_dump return fi uci delete "$UCI_CONFIG.$id" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # List redirects method_list_redirects() { json_init json_add_array "redirects" config_load "$UCI_CONFIG" config_foreach _add_redirect redirect json_close_array json_dump } _add_redirect() { local section="$1" local name match_host target_host strip_www code enabled config_get name "$section" name "$section" config_get match_host "$section" match_host "" config_get target_host "$section" target_host "" config_get strip_www "$section" strip_www "0" config_get code "$section" code "301" config_get enabled "$section" enabled "1" json_add_object json_add_string "id" "$section" json_add_string "name" "$name" json_add_string "match_host" "$match_host" json_add_string "target_host" "$target_host" json_add_boolean "strip_www" "$strip_www" json_add_int "code" "$code" json_add_boolean "enabled" "$enabled" json_close_object } # Create redirect method_create_redirect() { local name match_host target_host strip_www code enabled local section_id read -r input json_load "$input" json_get_var name name json_get_var match_host match_host json_get_var target_host target_host json_get_var strip_www strip_www "0" json_get_var code code "301" json_get_var enabled enabled "1" if [ -z "$name" ] || [ -z "$match_host" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Name and match_host are required" json_dump return fi section_id="redirect_$(echo "$name" | sed 's/[^a-zA-Z0-9]/_/g')" uci set "$UCI_CONFIG.$section_id=redirect" uci set "$UCI_CONFIG.$section_id.name=$name" uci set "$UCI_CONFIG.$section_id.match_host=$match_host" [ -n "$target_host" ] && uci set "$UCI_CONFIG.$section_id.target_host=$target_host" uci set "$UCI_CONFIG.$section_id.strip_www=$strip_www" uci set "$UCI_CONFIG.$section_id.code=$code" uci set "$UCI_CONFIG.$section_id.enabled=$enabled" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_add_string "id" "$section_id" json_dump } # Delete redirect method_delete_redirect() { local id read -r input json_load "$input" json_get_var id id if [ -z "$id" ]; then json_init json_add_boolean "success" 0 json_add_string "error" "Missing redirect id" json_dump return fi uci delete "$UCI_CONFIG.$id" uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Get settings method_get_settings() { json_init # Main settings json_add_object "main" json_add_boolean "enabled" "$(get_uci main enabled 0)" json_add_int "http_port" "$(get_uci main http_port 80)" json_add_int "https_port" "$(get_uci main https_port 443)" json_add_int "stats_port" "$(get_uci main stats_port 8404)" json_add_boolean "stats_enabled" "$(get_uci main stats_enabled 1)" json_add_string "stats_user" "$(get_uci main stats_user admin)" json_add_string "stats_password" "$(get_uci main stats_password secubox)" json_add_string "data_path" "$(get_uci main data_path /srv/haproxy)" json_add_string "memory_limit" "$(get_uci main memory_limit 256M)" json_add_int "maxconn" "$(get_uci main maxconn 4096)" json_add_string "log_level" "$(get_uci main log_level warning)" json_close_object # Defaults json_add_object "defaults" json_add_string "mode" "$(get_uci defaults mode http)" json_add_string "timeout_connect" "$(get_uci defaults timeout_connect 5s)" json_add_string "timeout_client" "$(get_uci defaults timeout_client 30s)" json_add_string "timeout_server" "$(get_uci defaults timeout_server 30s)" json_add_string "timeout_http_request" "$(get_uci defaults timeout_http_request 10s)" json_add_string "timeout_http_keep_alive" "$(get_uci defaults timeout_http_keep_alive 10s)" json_add_int "retries" "$(get_uci defaults retries 3)" json_close_object # ACME settings json_add_object "acme" json_add_boolean "enabled" "$(get_uci acme enabled 1)" json_add_string "email" "$(get_uci acme email admin@example.com)" json_add_boolean "staging" "$(get_uci acme staging 0)" json_add_string "key_type" "$(get_uci acme key_type ec-256)" json_add_int "renew_days" "$(get_uci acme renew_days 30)" json_close_object json_dump } # Save settings method_save_settings() { read -r input json_load "$input" # Main settings json_select "main" 2>/dev/null && { local val json_get_var val enabled && uci set "$UCI_CONFIG.main.enabled=$val" json_get_var val http_port && uci set "$UCI_CONFIG.main.http_port=$val" json_get_var val https_port && uci set "$UCI_CONFIG.main.https_port=$val" json_get_var val stats_port && uci set "$UCI_CONFIG.main.stats_port=$val" json_get_var val stats_enabled && uci set "$UCI_CONFIG.main.stats_enabled=$val" json_get_var val stats_user && uci set "$UCI_CONFIG.main.stats_user=$val" json_get_var val stats_password && uci set "$UCI_CONFIG.main.stats_password=$val" json_get_var val data_path && uci set "$UCI_CONFIG.main.data_path=$val" json_get_var val memory_limit && uci set "$UCI_CONFIG.main.memory_limit=$val" json_get_var val maxconn && uci set "$UCI_CONFIG.main.maxconn=$val" json_get_var val log_level && uci set "$UCI_CONFIG.main.log_level=$val" json_select .. } # Defaults json_select "defaults" 2>/dev/null && { local val json_get_var val mode && uci set "$UCI_CONFIG.defaults.mode=$val" json_get_var val timeout_connect && uci set "$UCI_CONFIG.defaults.timeout_connect=$val" json_get_var val timeout_client && uci set "$UCI_CONFIG.defaults.timeout_client=$val" json_get_var val timeout_server && uci set "$UCI_CONFIG.defaults.timeout_server=$val" json_get_var val timeout_http_request && uci set "$UCI_CONFIG.defaults.timeout_http_request=$val" json_get_var val timeout_http_keep_alive && uci set "$UCI_CONFIG.defaults.timeout_http_keep_alive=$val" json_get_var val retries && uci set "$UCI_CONFIG.defaults.retries=$val" json_select .. } # ACME settings json_select "acme" 2>/dev/null && { local val json_get_var val enabled && uci set "$UCI_CONFIG.acme.enabled=$val" json_get_var val email && uci set "$UCI_CONFIG.acme.email=$val" json_get_var val staging && uci set "$UCI_CONFIG.acme.staging=$val" json_get_var val key_type && uci set "$UCI_CONFIG.acme.key_type=$val" json_get_var val renew_days && uci set "$UCI_CONFIG.acme.renew_days=$val" json_select .. } uci commit "$UCI_CONFIG" run_ctl generate >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Service control: install method_install() { local result result=$(run_ctl install 2>&1) local rc=$? json_init if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "HAProxy installed successfully" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump } # Service control: start method_start() { /etc/init.d/haproxy start >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Service control: stop method_stop() { /etc/init.d/haproxy stop >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Service control: restart method_restart() { /etc/init.d/haproxy restart >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Service control: reload method_reload() { run_ctl reload >/dev/null 2>&1 json_init json_add_boolean "success" 1 json_dump } # Generate config method_generate() { local result result=$(run_ctl generate 2>&1) local rc=$? json_init if [ $rc -eq 0 ]; then json_add_boolean "success" 1 json_add_string "message" "Configuration generated" else json_add_boolean "success" 0 json_add_string "error" "$result" fi json_dump } # Validate config method_validate() { local result result=$(run_ctl validate 2>&1) local rc=$? json_init if [ $rc -eq 0 ]; then json_add_boolean "valid" 1 json_add_string "message" "Configuration is valid" else json_add_boolean "valid" 0 json_add_string "error" "$result" fi json_dump } # Get logs method_get_logs() { local lines read -r input json_load "$input" json_get_var lines lines "100" local logs logs=$(logread -l "$lines" 2>/dev/null | grep -i haproxy || echo "No HAProxy logs found") json_init json_add_string "logs" "$logs" json_dump } # List exposed services (from secubox-exposure config) method_list_exposed_services() { json_init json_add_array "services" # Clear temp file for tracking known service ports rm -f /tmp/.known_service_ports touch /tmp/.known_service_ports # Load known services from exposure config if uci -q show secubox-exposure >/dev/null 2>&1; then config_load "secubox-exposure" config_foreach _add_exposed_service known fi # Also scan listening ports for dynamic discovery if command -v netstat >/dev/null 2>&1; then # Save netstat output to temp file to avoid subshell issues netstat -tlnp 2>/dev/null | grep LISTEN > /tmp/.netstat_listen while read line; do local addr_port=$(echo "$line" | awk '{print $4}') local port=$(echo "$addr_port" | awk -F: '{print $NF}') local proc=$(echo "$line" | awk '{print $7}' | cut -d'/' -f2) # Skip common system ports case "$port" in 22|53|80|443|8404) continue ;; esac # Skip if already added from known services grep -q "^${port}$" /tmp/.known_service_ports 2>/dev/null && continue # Only add if process name is useful if [ -n "$proc" ] && [ "$proc" != "-" ] && [ "$proc" != "unknown" ]; then json_add_object json_add_string "id" "dynamic_${proc}_${port}" json_add_string "name" "$proc" json_add_int "port" "$port" json_add_string "address" "127.0.0.1" json_add_string "category" "detected" json_add_boolean "dynamic" 1 json_add_boolean "running" 1 json_close_object fi done < /tmp/.netstat_listen rm -f /tmp/.netstat_listen fi # Cleanup rm -f /tmp/.known_service_ports json_close_array json_dump } _add_exposed_service() { local section="$1" local default_port config_path config_file category process_name description local reserved_port listening running config_get default_port "$section" default_port "" config_get config_path "$section" config_path "" config_get config_file "$section" config_file "" config_get category "$section" category "app" config_get process_name "$section" process_name "" config_get description "$section" description "" [ -z "$default_port" ] && return # Get reserved port from UCI config (takes precedence over default) reserved_port="$default_port" if [ -n "$config_path" ]; then local configured_port=$(uci -q get "$config_path" 2>/dev/null) [ -n "$configured_port" ] && reserved_port="$configured_port" fi # For YAML config files, try to extract port if [ -n "$config_file" ] && [ -f "$config_file" ]; then local yaml_port=$(grep -E "^\s*port:\s*[0-9]+" "$config_file" 2>/dev/null | head -1 | awk '{print $2}') [ -n "$yaml_port" ] && reserved_port="$yaml_port" fi # Check if reserved port is actually listening listening=0 running=0 if netstat -tlnp 2>/dev/null | grep -qE ":${reserved_port}\s"; then listening=1 # Verify process name if specified if [ -n "$process_name" ]; then if netstat -tlnp 2>/dev/null | grep ":${reserved_port}\s" | grep -q "$process_name"; then running=1 fi else running=1 fi fi # Track reserved ports for dedup (only reserve what's configured) echo "$reserved_port" >> /tmp/.known_service_ports json_add_object json_add_string "id" "$section" json_add_string "name" "$section" json_add_int "port" "$reserved_port" json_add_string "address" "127.0.0.1" json_add_string "category" "$category" json_add_boolean "dynamic" 0 json_add_boolean "listening" "$listening" json_add_boolean "running" "$running" [ -n "$description" ] && json_add_string "description" "$description" [ -n "$process_name" ] && json_add_string "process" "$process_name" json_close_object } # Main RPC interface case "$1" in list) cat <<'EOF' { "status": {}, "get_stats": {}, "list_vhosts": {}, "get_vhost": { "id": "string" }, "create_vhost": { "domain": "string", "backend": "string", "ssl": "boolean", "ssl_redirect": "boolean", "acme": "boolean", "enabled": "boolean" }, "update_vhost": { "id": "string", "domain": "string", "backend": "string", "ssl": "boolean", "ssl_redirect": "boolean", "acme": "boolean", "enabled": "boolean" }, "delete_vhost": { "id": "string" }, "list_backends": {}, "get_backend": { "id": "string" }, "create_backend": { "name": "string", "mode": "string", "balance": "string", "health_check": "string", "health_check_uri": "string", "enabled": "boolean" }, "update_backend": { "id": "string", "name": "string", "mode": "string", "balance": "string", "health_check": "string", "health_check_uri": "string", "enabled": "boolean" }, "delete_backend": { "id": "string" }, "list_servers": { "backend": "string" }, "create_server": { "backend": "string", "name": "string", "address": "string", "port": "integer", "weight": "integer", "check": "boolean", "enabled": "boolean" }, "update_server": { "id": "string", "backend": "string", "name": "string", "address": "string", "port": "integer", "weight": "integer", "check": "boolean", "enabled": "boolean", "inline": "boolean" }, "delete_server": { "id": "string", "inline": "boolean" }, "list_certificates": {}, "request_certificate": { "domain": "string" }, "start_cert_request": { "domain": "string", "staging": "boolean" }, "get_cert_task": { "task_id": "string" }, "list_cert_tasks": {}, "clean_cert_tasks": {}, "import_certificate": { "domain": "string", "cert": "string", "key": "string" }, "delete_certificate": { "id": "string" }, "list_acls": {}, "create_acl": { "name": "string", "type": "string", "pattern": "string", "backend": "string", "enabled": "boolean" }, "update_acl": { "id": "string", "name": "string", "type": "string", "pattern": "string", "backend": "string", "enabled": "boolean" }, "delete_acl": { "id": "string" }, "list_redirects": {}, "create_redirect": { "name": "string", "match_host": "string", "target_host": "string", "strip_www": "boolean", "code": "integer", "enabled": "boolean" }, "delete_redirect": { "id": "string" }, "get_settings": {}, "save_settings": { "main": "object", "defaults": "object", "acme": "object" }, "install": {}, "start": {}, "stop": {}, "restart": {}, "reload": {}, "generate": {}, "validate": {}, "get_logs": { "lines": "integer" }, "list_exposed_services": {} } EOF ;; call) case "$2" in status) method_status ;; get_stats) method_get_stats ;; list_vhosts) method_list_vhosts ;; get_vhost) method_get_vhost ;; create_vhost) method_create_vhost ;; update_vhost) method_update_vhost ;; delete_vhost) method_delete_vhost ;; list_backends) method_list_backends ;; get_backend) method_get_backend ;; create_backend) method_create_backend ;; update_backend) method_update_backend ;; delete_backend) method_delete_backend ;; list_servers) method_list_servers ;; create_server) method_create_server ;; update_server) method_update_server ;; delete_server) method_delete_server ;; list_certificates) method_list_certificates ;; request_certificate) method_request_certificate ;; start_cert_request) method_start_cert_request ;; get_cert_task) method_get_cert_task ;; list_cert_tasks) method_list_cert_tasks ;; clean_cert_tasks) method_clean_cert_tasks ;; import_certificate) method_import_certificate ;; delete_certificate) method_delete_certificate ;; list_acls) method_list_acls ;; create_acl) method_create_acl ;; update_acl) method_update_acl ;; delete_acl) method_delete_acl ;; list_redirects) method_list_redirects ;; create_redirect) method_create_redirect ;; delete_redirect) method_delete_redirect ;; get_settings) method_get_settings ;; save_settings) method_save_settings ;; install) method_install ;; start) method_start ;; stop) method_stop ;; restart) method_restart ;; reload) method_reload ;; generate) method_generate ;; validate) method_validate ;; get_logs) method_get_logs ;; list_exposed_services) method_list_exposed_services ;; esac ;; esac