secubox-openwrt/luci-app-vhost-manager/root/usr/libexec/rpcd/luci.vhost-manager
2025-12-29 09:03:49 +01:00

586 lines
14 KiB
Bash
Executable File

#!/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