575 lines
14 KiB
Bash
Executable File
575 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
|
|
|
|
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" "0.4.1"
|
|
|
|
# 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
|