#!/bin/sh # RPCD backend for VHost Manager # Provides ubus interface: luci.vhost-manager . /lib/functions.sh . /usr/share/libubox/jshn.sh get_pkg_version() { local ctrl="/usr/lib/opkg/info/luci-app-vhost-manager.control" if [ -f "$ctrl" ]; then awk -F': ' '/^Version/ { print $2; exit }' "$ctrl" else echo "unknown" fi } PKG_VERSION="$(get_pkg_version)" NGINX_VHOST_DIR="/etc/nginx/conf.d" ACME_STATE_DIR="/etc/acme" VHOST_CONFIG="/etc/config/vhost_manager" # Initialize directories init_dirs() { mkdir -p "$NGINX_VHOST_DIR" mkdir -p "$ACME_STATE_DIR" touch "$VHOST_CONFIG" } # Generate nginx vhost configuration generate_vhost_config() { local domain="$1" local backend="$2" local ssl="$3" local auth="$4" local websocket="$5" local config_file="${NGINX_VHOST_DIR}/${domain}.conf" cat > "$config_file" << NGINXEOF # VHost for ${domain} # Generated by LuCI VHost Manager server { listen 80; server_name ${domain}; NGINXEOF # Add SSL redirect if enabled if [ "$ssl" = "1" ]; then cat >> "$config_file" << NGINXEOF # Redirect to HTTPS return 301 https://\$host\$request_uri; } server { listen 443 ssl http2; server_name ${domain}; # SSL certificates ssl_certificate /etc/acme/${domain}/fullchain.cer; ssl_certificate_key /etc/acme/${domain}/${domain}.key; # SSL settings ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; NGINXEOF fi # Add authentication if enabled if [ "$auth" = "1" ]; then cat >> "$config_file" << NGINXEOF # Basic authentication auth_basic "Restricted Access"; auth_basic_user_file /etc/nginx/.htpasswd_${domain}; NGINXEOF fi # Add proxy configuration cat >> "$config_file" << NGINXEOF location / { proxy_pass ${backend}; proxy_http_version 1.1; # Proxy headers proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; NGINXEOF # Add WebSocket support if enabled if [ "$websocket" = "1" ]; then cat >> "$config_file" << NGINXEOF # WebSocket support proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection "upgrade"; NGINXEOF fi cat >> "$config_file" << NGINXEOF # Timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } # Access logs access_log /var/log/nginx/${domain}_access.log; error_log /var/log/nginx/${domain}_error.log; } NGINXEOF echo "$config_file" } # Test backend connectivity test_backend() { local backend="$1" # Extract host and port from backend URL local host=$(echo "$backend" | sed 's|https\?://||' | cut -d':' -f1 | cut -d'/' -f1) local port=$(echo "$backend" | sed 's|https\?://||' | cut -d':' -f2 | cut -d'/' -f1) # Default port if not specified if [ "$port" = "$host" ]; then if echo "$backend" | grep -q "^https"; then port=443 else port=80 fi fi # Test connection if nc -z -w 5 "$host" "$port" 2>/dev/null; then return 0 else return 1 fi } case "$1" in list) json_init json_add_object "status" json_close_object json_add_object "list_vhosts" json_close_object json_add_object "get_vhost" json_add_string "domain" "string" json_close_object json_add_object "add_vhost" json_add_string "domain" "string" json_add_string "backend" "string" json_add_string "ssl" "bool" json_add_string "auth" "bool" json_add_string "websocket" "bool" json_close_object json_add_object "update_vhost" json_add_string "domain" "string" json_add_string "backend" "string" json_add_string "ssl" "bool" json_add_string "auth" "bool" json_add_string "websocket" "bool" json_close_object json_add_object "delete_vhost" json_add_string "domain" "string" json_close_object json_add_object "test_backend" json_add_string "backend" "string" json_close_object json_add_object "request_cert" json_add_string "domain" "string" json_add_string "email" "string" json_close_object json_add_object "list_certs" json_close_object json_add_object "reload_nginx" json_close_object json_add_object "get_access_logs" json_add_string "domain" "string" json_add_string "lines" "int" json_close_object json_dump ;; call) case "$2" in status) init_dirs json_init json_add_boolean "enabled" 1 json_add_string "module" "vhost-manager" json_add_string "version" "$PKG_VERSION" # Check nginx status if pgrep -x nginx > /dev/null 2>&1; then json_add_boolean "nginx_running" 1 # Get nginx version local nginx_version=$(nginx -v 2>&1 | grep -o 'nginx/[0-9.]*' | cut -d'/' -f2) json_add_string "nginx_version" "$nginx_version" else json_add_boolean "nginx_running" 0 json_add_string "nginx_version" "unknown" fi # Check acme.sh availability if command -v acme.sh > /dev/null 2>&1; then json_add_boolean "acme_available" 1 local acme_version=$(acme.sh --version 2>/dev/null | head -1) json_add_string "acme_version" "$acme_version" else json_add_boolean "acme_available" 0 json_add_string "acme_version" "not installed" fi # Count vhosts local vhost_count=0 if [ -d "$NGINX_VHOST_DIR" ]; then vhost_count=$(find "$NGINX_VHOST_DIR" -name "*.conf" -type f 2>/dev/null | wc -l) fi json_add_int "vhost_count" "$vhost_count" json_dump ;; list_vhosts) init_dirs json_init json_add_array "vhosts" if [ -d "$NGINX_VHOST_DIR" ]; then find "$NGINX_VHOST_DIR" -name "*.conf" -type f 2>/dev/null | while read -r conf_file; do local domain=$(basename "$conf_file" .conf) # Parse config to extract info local ssl=0 local auth=0 local websocket=0 local backend="unknown" if grep -q "listen 443 ssl" "$conf_file"; then ssl=1 fi if grep -q "auth_basic" "$conf_file"; then auth=1 fi if grep -q "Upgrade.*http_upgrade" "$conf_file"; then websocket=1 fi backend=$(grep "proxy_pass" "$conf_file" | head -1 | sed 's/.*proxy_pass\s*\([^;]*\);.*/\1/') # Check SSL cert expiry local ssl_expires="N/A" if [ "$ssl" = "1" ] && [ -f "/etc/acme/${domain}/fullchain.cer" ]; then ssl_expires=$(openssl x509 -in "/etc/acme/${domain}/fullchain.cer" -noout -enddate 2>/dev/null | cut -d'=' -f2) fi json_add_object json_add_string "domain" "$domain" json_add_string "backend" "$backend" json_add_boolean "ssl" "$ssl" json_add_boolean "auth" "$auth" json_add_boolean "websocket" "$websocket" json_add_string "ssl_expires" "$ssl_expires" json_add_string "config_file" "$conf_file" json_close_object done fi json_close_array json_dump ;; get_vhost) read -r input json_load "$input" json_get_var domain domain local config_file="${NGINX_VHOST_DIR}/${domain}.conf" json_init json_add_string "domain" "$domain" if [ -f "$config_file" ]; then json_add_boolean "exists" 1 # Parse configuration local ssl=0 local auth=0 local websocket=0 local backend="unknown" if grep -q "listen 443 ssl" "$config_file"; then ssl=1 fi if grep -q "auth_basic" "$config_file"; then auth=1 fi if grep -q "Upgrade.*http_upgrade" "$config_file"; then websocket=1 fi backend=$(grep "proxy_pass" "$config_file" | head -1 | sed 's/.*proxy_pass\s*\([^;]*\);.*/\1/') json_add_string "backend" "$backend" json_add_boolean "ssl" "$ssl" json_add_boolean "auth" "$auth" json_add_boolean "websocket" "$websocket" json_add_string "config_file" "$config_file" # SSL certificate info if [ "$ssl" = "1" ] && [ -f "/etc/acme/${domain}/fullchain.cer" ]; then local ssl_expires=$(openssl x509 -in "/etc/acme/${domain}/fullchain.cer" -noout -enddate 2>/dev/null | cut -d'=' -f2) local ssl_issuer=$(openssl x509 -in "/etc/acme/${domain}/fullchain.cer" -noout -issuer 2>/dev/null | cut -d'=' -f2-) json_add_string "ssl_expires" "$ssl_expires" json_add_string "ssl_issuer" "$ssl_issuer" fi else json_add_boolean "exists" 0 fi json_dump ;; add_vhost) read -r input json_load "$input" json_get_var domain domain json_get_var backend backend json_get_var ssl ssl json_get_var auth auth json_get_var websocket websocket init_dirs # Validate domain if [ -z "$domain" ] || [ -z "$backend" ]; then json_init json_add_boolean "success" 0 json_add_string "message" "Domain and backend are required" json_dump exit 0 fi # Check if vhost already exists if [ -f "${NGINX_VHOST_DIR}/${domain}.conf" ]; then json_init json_add_boolean "success" 0 json_add_string "message" "VHost already exists for domain: $domain" json_dump exit 0 fi # Generate nginx config local config_file=$(generate_vhost_config "$domain" "$backend" "$ssl" "$auth" "$websocket") # Test nginx config if nginx -t 2>&1 | grep -q "successful"; then json_init json_add_boolean "success" 1 json_add_string "message" "VHost created successfully" json_add_string "domain" "$domain" json_add_string "config_file" "$config_file" json_add_boolean "reload_required" 1 json_dump else # Remove invalid config rm -f "$config_file" json_init json_add_boolean "success" 0 json_add_string "message" "Invalid nginx configuration" json_dump fi ;; update_vhost) read -r input json_load "$input" json_get_var domain domain json_get_var backend backend json_get_var ssl ssl json_get_var auth auth json_get_var websocket websocket # Check if vhost exists if [ ! -f "${NGINX_VHOST_DIR}/${domain}.conf" ]; then json_init json_add_boolean "success" 0 json_add_string "message" "VHost not found: $domain" json_dump exit 0 fi # Backup old config cp "${NGINX_VHOST_DIR}/${domain}.conf" "${NGINX_VHOST_DIR}/${domain}.conf.bak" # Generate new config local config_file=$(generate_vhost_config "$domain" "$backend" "$ssl" "$auth" "$websocket") # Test nginx config if nginx -t 2>&1 | grep -q "successful"; then rm -f "${NGINX_VHOST_DIR}/${domain}.conf.bak" json_init json_add_boolean "success" 1 json_add_string "message" "VHost updated successfully" json_add_boolean "reload_required" 1 json_dump else # Restore backup mv "${NGINX_VHOST_DIR}/${domain}.conf.bak" "${NGINX_VHOST_DIR}/${domain}.conf" json_init json_add_boolean "success" 0 json_add_string "message" "Invalid configuration, changes reverted" json_dump fi ;; delete_vhost) read -r input json_load "$input" json_get_var domain domain local config_file="${NGINX_VHOST_DIR}/${domain}.conf" if [ -f "$config_file" ]; then rm -f "$config_file" json_init json_add_boolean "success" 1 json_add_string "message" "VHost deleted: $domain" json_add_boolean "reload_required" 1 json_dump else json_init json_add_boolean "success" 0 json_add_string "message" "VHost not found: $domain" json_dump fi ;; test_backend) read -r input json_load "$input" json_get_var backend backend json_init json_add_string "backend" "$backend" if test_backend "$backend"; then json_add_boolean "reachable" 1 json_add_string "status" "Backend is reachable" else json_add_boolean "reachable" 0 json_add_string "status" "Backend is unreachable" fi json_dump ;; request_cert) read -r input json_load "$input" json_get_var domain domain json_get_var email email json_init if ! command -v acme.sh > /dev/null 2>&1; then json_add_boolean "success" 0 json_add_string "message" "acme.sh not installed" json_dump exit 0 fi # Request certificate using acme.sh if acme.sh --issue -d "$domain" --standalone --force 2>&1 | grep -q "success"; then json_add_boolean "success" 1 json_add_string "message" "Certificate requested successfully" json_add_string "domain" "$domain" else json_add_boolean "success" 0 json_add_string "message" "Certificate request failed" fi json_dump ;; list_certs) json_init json_add_array "certificates" if [ -d "$ACME_STATE_DIR" ]; then find "$ACME_STATE_DIR" -name "fullchain.cer" -type f 2>/dev/null | while read -r cert_file; do local domain=$(basename $(dirname "$cert_file")) local expires=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d'=' -f2) local issuer=$(openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | cut -d'=' -f2-) local subject=$(openssl x509 -in "$cert_file" -noout -subject 2>/dev/null | cut -d'=' -f2-) json_add_object json_add_string "domain" "$domain" json_add_string "expires" "$expires" json_add_string "issuer" "$issuer" json_add_string "subject" "$subject" json_add_string "cert_file" "$cert_file" json_close_object done fi json_close_array json_dump ;; reload_nginx) json_init # Test configuration first if nginx -t 2>&1 | grep -q "successful"; then # Reload nginx if /etc/init.d/nginx reload 2>&1; then json_add_boolean "success" 1 json_add_string "message" "Nginx reloaded successfully" else json_add_boolean "success" 0 json_add_string "message" "Failed to reload nginx" fi else json_add_boolean "success" 0 json_add_string "message" "Invalid nginx configuration" fi json_dump ;; get_access_logs) read -r input json_load "$input" json_get_var domain domain json_get_var lines lines lines=${lines:-50} local log_file="/var/log/nginx/${domain}_access.log" json_init json_add_string "domain" "$domain" json_add_array "logs" if [ -f "$log_file" ]; then tail -n "$lines" "$log_file" | while read -r log_line; do json_add_string "" "$log_line" done fi json_close_array json_dump ;; *) json_init json_add_int "error" -32601 json_add_string "message" "Method not found: $2" json_dump ;; esac ;; esac