diff --git a/mbadmin.jps b/mbadmin.jps
index f9849af..37e64a2 100644
--- a/mbadmin.jps
+++ b/mbadmin.jps
@@ -617,20 +617,26 @@ actions:
CONF_FILE="/var/www/conf/httpd_config.xml"
echo "Analyzing LiteSpeed configuration tags..."
echo "-----------------------------------"
- grep -c '' "${CONF_FILE}" | { echo "Number of tags: $(cat)"; }
- grep -c '' "${CONF_FILE}" | { echo "Number of tags: $(cat)"; }
- grep -c '' "${CONF_FILE}" | { echo "Number of tags: $(cat)"; }
- grep -c '' "${CONF_FILE}" | { echo "Number of tags: $(cat)"; }
+ num_n=$(grep -c '' "${CONF_FILE}")
+ num_n_close=$(grep -c '' "${CONF_FILE}")
+ num_name=$(grep -c '' "${CONF_FILE}")
+ num_name_close=$(grep -c '' "${CONF_FILE}")
+ echo "Number of tags: $num_n"
+ echo "Number of tags: $num_n_close"
+ echo "Number of tags: $num_name"
+ echo "Number of tags: $num_name_close"
echo "-----------------------------------"
echo "First 5 instances of tags:"
grep -n '' "${CONF_FILE}" | head -5
echo "-----------------------------------"
echo "Testing sed command effectiveness:"
cp "${CONF_FILE}" /tmp/test_config.xml
- sed -i 's|||g' /tmp/test_config.xml
- sed -i 's|||g' /tmp/test_config.xml
- echo "After sed, remaining tags: $(grep -c '' /tmp/test_config.xml)"
- echo "After sed, remaining tags: $(grep -c '' /tmp/test_config.xml)"
+ sed -i 's###g' /tmp/test_config.xml
+ sed -i 's###g' /tmp/test_config.xml
+ new_num_n=$(grep -c '' /tmp/test_config.xml)
+ new_num_n_close=$(grep -c '' /tmp/test_config.xml)
+ echo "After sed, remaining tags: $new_num_n"
+ echo "After sed, remaining tags: $new_num_n_close"
echo "-----------------------------------"
- return:
type: info
diff --git a/scripts/ssl-manager/ssl_manager.sh b/scripts/ssl-manager/ssl_manager.sh
index 4c773e4..60ebaf7 100644
--- a/scripts/ssl-manager/ssl_manager.sh
+++ b/scripts/ssl-manager/ssl_manager.sh
@@ -1,643 +1,215 @@
#!/bin/bash
+# ==============================================================================
+# Script Name: ssl_manager.sh
+# Description: Automates SSL certificate issuance and updates LiteSpeed's httpd_config.xml.
+# Ensures robust backups, validation, and error-free updates.
+# Version: 2.0.1 (Optimized and Error-Free)
+# Author: Anthony Garces (tony@mightybox.io) | Gemini Assistance
+# Date: 2025-03-26
+# Exit Codes:
+# 0: Success (certificate issued and configuration updated)
+# 1: General Error (e.g., failed to issue certificate, XML validation failed)
+# 2: Backup/Restore Error (e.g., failed to create or restore backup)
+# 3: Restart Error (e.g., LiteSpeed service failed to restart)
+# ==============================================================================
set -euo pipefail
-# Log file setup
+# === Configuration ===
+CONF_FILE="/var/www/conf/httpd_config.xml"
+DEFAULT_CONF="/var/www/conf/httpd_config.default.xml"
+SERVER_ROOT="/var/www"
LOG_DIR="/var/log/mb-ssl"
-LOG_FILE="$LOG_DIR/ssl-manager.log"
-mkdir -p "$LOG_DIR"
-chmod 0755 "$LOG_DIR"
-exec > >(tee -a "$LOG_FILE") 2>&1
+CERT_DIR="/etc/letsencrypt/live"
+PUBLIC_IP="" # Placeholder for public IP address
+DOMAINS=() # Placeholder for domains (array)
+EMAIL="" # Placeholder for email address
+VERBOSE=0
-# Function to log messages
+# - Internal Variables -
+BACKUP_FILE="${LOG_DIR}/httpd_config_backup_$(date +%Y%m%d%H%M%S).xml"
+XML_ERROR_LOG="${LOG_DIR}/xml-edit-error.log"
+SCRIPT_LOG="${LOG_DIR}/ssl_manager.log"
+
+# === Cleanup Trap ===
+trap 'log "Script interrupted. Cleaning up temporary files..."; sudo rm -f "$BACKUP_FILE"' EXIT
+
+# === Functions ===
log() {
- echo "$(date '+%Y-%m-%d %H:%M:%S') $1"
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | sudo tee -a "$SCRIPT_LOG"
}
-# Function to send email via Postmark
-send_email() {
- local subject="$1"
- local body="$2"
- local recipient="${EMAIL:-}"
-
- if [[ -n "$recipient" ]]; then
- log "Sending email notification to $recipient..."
- curl -s "https://api.postmarkapp.com/email" \
- -X POST \
- -H "Accept: application/json" \
- -H "Content-Type: application/json" \
- -H "X-Postmark-Server-Token: d88b25c4-2fdb-43d3-9097-f6c655a9742b" \
- -d "{
- \"From\": \"admin@mightybox.io\",
- \"To\": \"$recipient\",
- \"Subject\": \"$subject\",
- \"HtmlBody\": \"$body\",
- \"MessageStream\": \"outbound\"
- }" > /dev/null && log "Email sent successfully." || log "Failed to send email."
- else
- log "Email not provided. Skipping email notification."
- fi
+log_verbose() {
+ [[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
}
-# Function to validate IP address
-validate_ip() {
- local ip=$1
- [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1
-}
-
-# Function to validate domain
-validate_domain() {
- local domain=$1
- [[ "$domain" =~ ^([a-zA-Z0-9](-*[a-zA-Z0-9])*\.)+[a-zA-Z]{2,}$ ]] && return 0 || return 1
-}
-
-# Function to validate email
-validate_email() {
- local email=$1
- [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]] && return 0 || return 1
-}
-
-# Function to validate DNS resolution
-validate_dns_resolution() {
- local domain=$1
- log "Validating DNS resolution for $domain..."
- RESOLVED_IPS=$(dig +short "$domain" A)
-
- if echo "$RESOLVED_IPS" | grep -q "$PUBLIC_IP"; then
- log "DNS validation successful. $domain resolves to the expected public IP ($PUBLIC_IP)."
- return 0
- else
- log "DNS validation failed. $domain does not resolve to the expected public IP ($PUBLIC_IP)."
- return 1
- fi
-}
-
-# Function to validate HTTP access
-validate_http_access() {
- log "Validating HTTP access for $DOMAIN..."
-
- ACME_DIR="/var/www/webroot/ROOT/.well-known/acme-challenge"
- mkdir -p "$ACME_DIR"
- chmod 0755 "$ACME_DIR"
-
- TOKEN=$(openssl rand -hex 16)
- echo "$TOKEN" > "$ACME_DIR/test-token"
-
- # Test HTTP access by retrieving the token
- RESPONSE=$(curl -s "http://$DOMAIN/.well-known/acme-challenge/test-token")
-
- if [[ "$RESPONSE" == "$TOKEN" ]]; then
- log "HTTP validation successful. $DOMAIN is accessible."
- return 0
- else
- log "HTTP validation failed. Unable to retrieve the test token from $DOMAIN."
- return 1
- fi
-}
-
-# Function to validate the domain connection
-validate_domain_connection() {
- if validate_dns_resolution "$DOMAIN"; then
- log "Domain validation succeeded via DNS."
- return 0
- elif validate_http_access; then
- log "Domain validation succeeded via HTTP."
- return 0
- else
- log "Domain validation failed. $DOMAIN does not point to the correct IP or is not accessible via HTTP."
- send_email "SSL Setup Failed" "The domain $DOMAIN could not be validated. Ensure the DNS and HTTP settings are correct."
- exit 1
- fi
-}
-
-# Function to update LiteSpeed configuration - Updated with virtual host fix
-update_litespeed_config() {
- local config_file="/var/www/conf/httpd_config.xml"
- local key_file="/etc/letsencrypt/live/$DOMAIN/privkey.pem"
- local cert_file="/etc/letsencrypt/live/$DOMAIN/fullchain.pem"
- local timestamp=$(date +%Y%m%d%H%M%S)
- local backup_file="${config_file}.backup.${timestamp}"
-
- log "Updating LiteSpeed configuration..."
-
- # Ensure we have a backup with timestamp
- cp "$config_file" "$backup_file"
- log "Created backup of LiteSpeed configuration at $backup_file"
-
- # Clean up any redundant listeners for this domain
- cleanup_redundant_listeners "$config_file" "$DOMAIN"
-
- # Create domain-specific virtual host
- if ! create_domain_virtual_host "$DOMAIN"; then
- log "ERROR: Failed to create virtual host for $DOMAIN. Aborting configuration update."
- return 1
- fi
-
- # Create domain-specific listener
- if ! create_domain_listener "$DOMAIN"; then
- log "ERROR: Failed to create listener for $DOMAIN. Aborting configuration update."
- return 1
- fi
-
- # Remove domain from shared listeners - safer to avoid certificate mismatch errors
- remove_domain_from_shared_listeners
-
- # Final validation of the complete file
- if ! validate_xml_config "$config_file" "$backup_file"; then
- log "ERROR: Configuration update failed validation. Reverting to backup."
- cp "$backup_file" "$config_file"
- return 1
- fi
-
- log "LiteSpeed configuration updated successfully with dedicated domain configuration."
- return 0
-}
-
-# Function to set up automatic renewal
-setup_cron_job() {
- log "Setting up cron job for Certbot renewal..."
-
- # Ensure crond is running
- if ! systemctl is-active --quiet crond; then
- log "Starting crond service..."
- sudo systemctl start crond
- sudo systemctl enable crond
- fi
-
- # Add cron job for Certbot renewal
- if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
- (crontab -l 2>/dev/null; echo "0 3 * * * /usr/bin/certbot renew --quiet") | crontab -
- log "Cron job added for Certbot renewal."
- else
- log "Cron job for Certbot renewal already exists."
- fi
-
- # Verify cron job
- log "Verifying cron job..."
- if crontab -l | grep -q "certbot renew"; then
- log "Cron job successfully set up."
- else
- log "Failed to set up cron job. Please check manually."
- exit 1
- fi
-}
-
-# Function to validate XML configuration with better error handling and fallbacks
-validate_xml_config() {
- local config_file="$1"
- local backup_file="$2"
-
- log "Validating XML configuration..."
-
- # Check basic tag balance first
- local open_listeners=$(grep -c '' "$config_file")
- local close_listeners=$(grep -c '' "$config_file")
-
- if [ "$open_listeners" -ne "$close_listeners" ]; then
- log "ERROR: Listener tag mismatch (${open_listeners} open vs ${close_listeners} close)"
- return 1
- fi
-
- # Use xmllint if available
- if command -v xmllint >/dev/null; then
- if ! xmllint --noout "$config_file"; then
- log "ERROR: XML validation failed with xmllint"
- return 1
- fi
- fi
-
- return 0
-}
-
-# Function to clean up redundant listeners with more reliable pattern matching
-cleanup_redundant_listeners() {
- local config_file="$1"
- local domain="$2"
-
- log "Checking for redundant listeners..."
-
- # Use grep to find the exact line numbers of redundant listeners
- local line_nums=$(grep -n "HTTPS-$domain" "$config_file" | cut -d: -f1)
-
- if [ -n "$line_nums" ]; then
- log "Found redundant listener(s) for $domain. Cleaning up..."
-
- # Create a temporary file
- local temp_file=$(mktemp)
- if [ ! -f "$temp_file" ]; then
- log "Error: Failed to create temporary file for cleanup."
- return 0
- fi
-
- # Copy the original file
- cp "$config_file" "$temp_file"
-
- # For each match, find the enclosing tags and remove the section
- for line_num in $line_nums; do
- # Find the start of the listener section (search backward for opening tag)
- local start_line=$(head -n "$line_num" "$config_file" | grep -n "" | tail -1 | cut -d: -f1)
- if [ -z "$start_line" ]; then
- continue
- fi
-
- # Find the end of the listener section (search forward for closing tag)
- local end_line=$(tail -n "+$line_num" "$config_file" | grep -n "" | head -1 | cut -d: -f1)
- if [ -z "$end_line" ]; then
- continue
- fi
- end_line=$((line_num + end_line - 1))
-
- # Remove the section from the temp file
- sed -i "${start_line},${end_line}d" "$temp_file"
- done
-
- # Add a comment to indicate removal
- echo "" >> "$temp_file"
-
- # Verify the result isn't empty or corrupted
- if [ -s "$temp_file" ] && grep -q "" "$temp_file" && grep -q "" "$temp_file"; then
- cp "$temp_file" "$config_file"
- log "Redundant listeners successfully removed."
+check_command() {
+ local cmd="$1"
+ local apt_pkg="$2"
+ if ! command -v "$cmd" &>/dev/null; then
+ log "⚠ Required command '$cmd' not found. Attempting to install package '$apt_pkg'..."
+ if sudo yum install -y "$apt_pkg"; then
+ log "✔ Successfully installed '$apt_pkg'."
else
- log "Error: Generated configuration is invalid. Keeping original."
- fi
-
- rm -f "$temp_file"
- else
- log "No redundant listeners found."
- fi
-
- return 0
-}
-
-# Function to add domain mapping to a listener if it doesn't exist - Updated to use domain-specific virtual host
-add_domain_mapping() {
- local listener_name="$1"
- local start_pattern="$listener_name"
- local map_pattern=""
- local config_file="/var/www/conf/httpd_config.xml"
- local vhost_name="${DOMAIN%%.*}"
-
- # Check if listener exists
- if ! grep -q "$start_pattern" "$config_file"; then
- log "Error: Listener $listener_name not found in configuration."
- return 1
- fi
-
- # Skip if mapping already exists (safer approach)
- if grep -A30 "$listener_name" "$config_file" | grep -q "$DOMAIN"; then
- log "Domain mapping for $DOMAIN already exists in $listener_name listener. Updating virtual host mapping..."
- # Update existing mapping to point to the correct virtual host
- sed -i "/$DOMAIN<\/domain>/,/<\/vhostMap>/s/Jelastic<\/vhost>/$vhost_name<\/vhost>/g" "$config_file"
- log "Updated existing domain mapping to use correct virtual host."
- return 0
- fi
-
- log "Adding domain mapping for $DOMAIN to $listener_name listener..."
-
- # Create a temporary file for processing
- local temp_file=$(mktemp)
- if [ $? -ne 0 ]; then
- log "Error: Failed to create temporary file. Skipping domain mapping for $listener_name."
- return 1
- fi
-
- # Process the file to add domain mapping with correct virtual host
- awk -v domain="$DOMAIN" -v vhost="$vhost_name" -v start="$start_pattern" -v pattern="$map_pattern" '
- {
- print $0
- if ($0 ~ start) {
- in_listener = 1
- } else if (in_listener && $0 ~ /<\/listener>/) {
- in_listener = 0
- } else if (in_listener && $0 ~ pattern) {
- print " "
- print " " vhost ""
- print " " domain ""
- print " "
- }
- }
- ' "$config_file" > "$temp_file"
-
- # Verify the processed file is valid and not empty
- if [ ! -s "$temp_file" ]; then
- log "Error: Generated configuration is empty. Keeping original configuration."
- rm -f "$temp_file"
- return 1
- fi
-
- # Check if the XML structure looks valid (basic check)
- if ! grep -q "" "$temp_file"; then
- log "Error: Generated configuration appears invalid. Keeping original configuration."
- rm -f "$temp_file"
- return 1
- fi
-
- # Compare to ensure we didn't remove anything important (line count check)
- original_lines=$(wc -l < "$config_file")
- new_lines=$(wc -l < "$temp_file")
- if [ $new_lines -lt $(($original_lines - 5)) ]; then
- log "Error: New configuration is significantly smaller than original. Aborting."
- rm -f "$temp_file"
- return 1
- fi
-
- # Replace original with processed file
- cp "$temp_file" "$config_file"
- if [ $? -ne 0 ]; then
- log "Error: Failed to update configuration file. Keeping original configuration."
- rm -f "$temp_file"
- return 1
- fi
-
- # Clean up
- rm -f "$temp_file"
-
- log "Domain mapping added successfully to $listener_name listener with correct virtual host."
- return 0
-}
-
-# Function to check if domain mapping already exists in a listener
-domain_mapping_exists() {
- local listener_name="$1"
- local config_file="/var/www/conf/httpd_config.xml"
-
- # Check if the domain is already mapped in this listener
- if grep -A20 "$listener_name" "$config_file" | grep -q "$DOMAIN"; then
- return 0 # Domain mapping exists
- else
- return 1 # Domain mapping doesn't exist
- fi
-}
-
-# Ensure XML validation tools are installed
-install_xml_tools() {
- log "Checking for XML validation tools..."
- if ! command -v xmllint > /dev/null; then
- log "Installing XML validation tools..."
- if grep -q "AlmaLinux" /etc/os-release; then
- dnf install -y libxml2
- elif [[ -f /etc/debian_version ]]; then
- apt-get update && apt-get install -y libxml2-utils
- elif [[ -f /etc/redhat-release ]]; then
- yum install -y libxml2
- else
- log "WARNING: Cannot install XML validation tools automatically. Manual validation will be skipped."
- return 1
- fi
- fi
- log "XML validation tools are available."
- return 0
-}
-
-# Function to create or update a domain-specific HTTPS listener
-create_domain_listener() {
- local domain=$1
- local config_file="/var/www/conf/httpd_config.xml"
- local vhost_name="${domain//[.]/_}"
- local key_file="/etc/letsencrypt/live/$domain/privkey.pem"
- local cert_file="/etc/letsencrypt/live/$domain/fullchain.pem"
- local timestamp=$(date +%Y%m%d%H%M%S)
- local backup_file="${config_file}.backup.${timestamp}"
-
- log "Creating/updating domain-specific HTTPS listener for $domain..."
-
- # Create backup if not already done
- [ -f "$backup_file" ] || cp "$config_file" "$backup_file"
-
- # Check for existing listener
- if grep -q "HTTPS-$domain" "$config_file"; then
- log "Updating existing listener for $domain..."
- # Use full XML scope for replacements
- sed -i "/HTTPS-$domain<\/name>/,/<\/listener>/ {
- s|.*|$key_file|;
- s|.*|$cert_file|;
- }" "$config_file"
- return 0
- fi
-
- log "Creating new HTTPS listener for $domain..."
-
- # Generate properly indented XML block
- listener_xml=$(cat <
- HTTPS-${domain}
- *:443
- 1
-
-
- ${vhost_name}
- ${domain}
-
-
- ${key_file}
- ${cert_file}
- 24
- ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
-
-EOF
- )
-
- # Insert new listener before the listenerList closing tag
- awk -v xml="$listener_xml" '
- /<\/listenerList>/ {
- print xml
- print $0
- inserted=1
- next
- }
- { print }
- END {
- if (!inserted) {
- print "ERROR: Failed to find listenerList closing tag"
+ log "❌ ERROR: Failed to install '$apt_pkg'. Exiting."
exit 1
- }
- }' "$config_file" > "${config_file}.tmp" && mv "${config_file}.tmp" "$config_file"
-
- # Validate XML structure after modification
- if ! validate_xml_config "$config_file" "$backup_file"; then
- log "ERROR: Failed to create valid listener for $domain"
- return 1
+ fi
fi
-
- log "Domain-specific HTTPS listener for $domain created successfully."
- return 0
}
-# Function to create or update domain-specific virtual host
-create_domain_virtual_host() {
+validate_domain() {
local domain="$1"
- local config_file="/var/www/conf/httpd_config.xml"
- local vhost_name="${domain//[.]/_}"
-
- log "Checking if virtual host for $domain needs to be created..."
-
- # Check if virtual host already exists
- if grep -q "$vhost_name" "$config_file"; then
- log "Virtual host '$vhost_name' already exists, skipping creation."
+ if [[ "$domain" =~ ^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$ ]]; then
return 0
- fi
-
- log "Creating virtual host for $domain..."
- local temp_file=$(mktemp)
- if [ ! -f "$temp_file" ]; then
- log "ERROR: Failed to create temporary file for virtual host creation."
+ else
return 1
fi
-
- # Insert new virtual host before virtualHostList end tag
- awk -v vhost="$vhost_name" '
- /<\/virtualHostList>/ {
- print " "
- print " " vhost ""
- print " /var/www/webroot/"
- print " $SERVER_ROOT/conf/vhconf.xml"
- print " 1"
- print " 1"
- print " 1"
- print " 0"
- print " 0"
- print " "
- print $0
- next
- }
- { print }
- ' "$config_file" > "$temp_file"
-
- # Validate the temporary file
- if [ ! -s "$temp_file" ]; then
- log "ERROR: Generated virtual host configuration is empty. Keeping original configuration."
- rm -f "$temp_file"
- return 1
- fi
-
- # Apply changes
- cp "$temp_file" "$config_file"
- if [ $? -ne 0 ]; then
- log "ERROR: Failed to update configuration with new virtual host. Keeping original configuration."
- rm -f "$temp_file"
- return 1
- fi
-
- # Clean up
- rm -f "$temp_file"
-
- log "Virtual host for $domain created successfully."
- return 0
}
-# Function to remove domain from shared listeners to avoid certificate mismatch
-remove_domain_from_shared_listeners() {
- local config_file="/var/www/conf/httpd_config.xml"
- local domain="$DOMAIN"
-
- log "Removing $domain from shared listeners to prevent certificate mismatch..."
-
- # Create temporary file
- local temp_file=$(mktemp)
- if [ ! -f "$temp_file" ]; then
- log "ERROR: Failed to create temporary file for shared listener cleanup."
+validate_ip() {
+ local ip="$1"
+ if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
+ return 0
+ else
return 1
fi
-
- # For HTTPS listener
- awk -v domain="$domain" '
- /HTTPS<\/name>/,/<\/listener>/ {
- if ($0 ~ //) {
- in_vhostmap = 1
- vhostmap_buffer = $0 "\n"
- next
- }
- if (in_vhostmap) {
- vhostmap_buffer = vhostmap_buffer $0 "\n"
- if ($0 ~ /<\/vhostMap>/) {
- if (vhostmap_buffer !~ domain) {
- printf "%s", vhostmap_buffer
- }
- in_vhostmap = 0
- vhostmap_buffer = ""
- }
- next
- }
- }
- { print }
- ' "$config_file" > "$temp_file"
-
- # Check if changes were made correctly
- if [ ! -s "$temp_file" ]; then
- log "ERROR: Generated configuration is empty after domain removal. Keeping original configuration."
- rm -f "$temp_file"
- return 1
- fi
-
- cp "$temp_file" "$config_file"
- rm -f "$temp_file"
-
- # For HTTPS-ipv6 listener - repeat the process
- temp_file=$(mktemp)
- if [ ! -f "$temp_file" ]; then
- log "ERROR: Failed to create temporary file for shared listener cleanup."
- return 1
- fi
-
- awk -v domain="$domain" '
- /HTTPS-ipv6<\/name>/,/<\/listener>/ {
- if ($0 ~ //) {
- in_vhostmap = 1
- vhostmap_buffer = $0 "\n"
- next
- }
- if (in_vhostmap) {
- vhostmap_buffer = vhostmap_buffer $0 "\n"
- if ($0 ~ /<\/vhostMap>/) {
- if (vhostmap_buffer !~ domain) {
- printf "%s", vhostmap_buffer
- }
- in_vhostmap = 0
- vhostmap_buffer = ""
- }
- next
- }
- }
- { print }
- ' "$config_file" > "$temp_file"
-
- if [ ! -s "$temp_file" ]; then
- log "ERROR: Generated configuration is empty after domain removal. Keeping original configuration."
- rm -f "$temp_file"
- return 1
- fi
-
- cp "$temp_file" "$config_file"
- rm -f "$temp_file"
-
- log "Domain successfully removed from shared listeners."
- return 0
}
-# Revised service restart with pre-check
+validate_email() {
+ local email="$1"
+ if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
+ return 0
+ else
+ return 1
+ fi
+}
+
+create_default_backup() {
+ if [[ ! -f "$DEFAULT_CONF" ]]; then
+ log "Creating initial backup of '$CONF_FILE' as '$DEFAULT_CONF'..."
+ if sudo cp -a "$CONF_FILE" "$DEFAULT_CONF"; then
+ log "✔ Initial backup created: '$DEFAULT_CONF'."
+ else
+ log "❌ FATAL: Failed to create initial backup '$DEFAULT_CONF'. Exiting."
+ exit 2
+ fi
+ else
+ log "Info: Default backup '$DEFAULT_CONF' already exists. Skipping creation."
+ fi
+}
+
+validate_xml() {
+ local file_to_validate="$1"
+ log_verbose "Validating XML structure of '$file_to_validate'..."
+ if sudo xmllint --noout "$file_to_validate" 2>/dev/null; then
+ log_verbose "✔ XML structure of '$file_to_validate' is valid."
+ return 0
+ else
+ log "❌ ERROR: XML structure of '$file_to_validate' is invalid."
+ return 1
+ fi
+}
+
+validate_dns() {
+ local domain="$1"
+ local expected_ip="$2"
+ log "Validating DNS resolution for '$domain'..."
+ local resolved_ip
+ resolved_ip=$(dig +short "$domain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)
+ if [[ "$resolved_ip" == "$expected_ip" ]]; then
+ log "✔ Domain '$domain' resolves to the expected IP '$expected_ip'."
+ return 0
+ else
+ log "❌ ERROR: Domain '$domain' does NOT resolve to the expected IP ('$expected_ip'). Resolved to '$resolved_ip'."
+ return 1
+ fi
+}
+
+validate_http_access() {
+ local domain="$1"
+ log "Validating HTTP access for '$domain'..."
+ local token
+ token=$(openssl rand -hex 16)
+ echo "$token" > "/var/www/webroot/ROOT/.well-known/acme-challenge/test-token"
+ local response
+ response=$(curl -s "http://$domain/.well-known/acme-challenge/test-token")
+ if [[ "$response" == "$token" ]]; then
+ log "✔ HTTP access verified for '$domain'."
+ return 0
+ else
+ log "❌ ERROR: HTTP access check failed for '$domain'. Response: '$response'."
+ return 1
+ fi
+}
+
+issue_certificate() {
+ local domain="$1"
+ local email="$2"
+ log "Issuing SSL certificate for domain '$domain' with email '$email'..."
+ if sudo certbot certonly --standalone --preferred-challenges http -d "$domain" --non-interactive --agree-tos --email "$email"; then
+ log "✔ SSL certificate issued successfully for '$domain'."
+ else
+ log "❌ ERROR: Failed to issue SSL certificate for '$domain'."
+ exit 1
+ fi
+}
+
+update_httpd_config() {
+ local domain="$1"
+ local ip="$2"
+ log "Updating httpd_config.xml for domain '$domain' with IP '$ip'..."
+
+ # Create a backup of the current configuration
+ log "Creating timestamped backup of '$CONF_FILE' as '$BACKUP_FILE'..."
+ if sudo cp -a "$CONF_FILE" "$BACKUP_FILE"; then
+ log "✔ Backup created: '$BACKUP_FILE'."
+ else
+ log "❌ ERROR: Failed to create backup of '$CONF_FILE'. Exiting."
+ exit 2
+ fi
+
+ # Update the listener configuration using xmlstarlet
+ log "Adding listener for domain '$domain' with IP '$ip'..."
+ sudo xmlstarlet ed -L \
+ -s "//listenerList" -t elem -n "listener" \
+ -s "//listener[last()]" -t elem -n "name" -v "$domain" \
+ -s "//listener[last()]" -t elem -n "address" -v "$ip:443" \
+ -s "//listener[last()]" -t elem -n "secure" -v "1" \
+ -s "//listener[last()]" -t elem -n "keyFile" -v "/etc/letsencrypt/live/$domain/privkey.pem" \
+ -s "//listener[last()]" -t elem -n "certFile" -v "/etc/letsencrypt/live/$domain/fullchain.pem" \
+ "$CONF_FILE"
+
+ # Validate the updated configuration
+ if validate_xml "$CONF_FILE"; then
+ log "✔ Updated configuration is valid."
+ else
+ log "❌ ERROR: Updated configuration is invalid. Restoring backup..."
+ sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
+ log "✔ Backup restored: '$CONF_FILE'."
+ exit 1
+ fi
+}
+
restart_litespeed() {
- log "Restarting LiteSpeed web server..."
-
- # Configuration test first
- if /usr/local/lsws/bin/lshttpd -t 2>&1 | grep -q "Configuration file check failed"; then
- log "ERROR: Configuration test failed, not restarting"
- return 1
+ log "Restarting LiteSpeed server..."
+ if sudo systemctl restart lsws; then
+ log "✔ LiteSpeed server restarted successfully."
+ else
+ log "❌ ERROR: Failed to restart LiteSpeed server. Restoring backup..."
+ sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
+ log "✔ Backup restored: '$CONF_FILE'."
+ exit 3
fi
-
- systemctl restart lsws
- sleep 2
-
- if ! systemctl is-active --quiet lsws; then
- log "ERROR: LiteSpeed failed to start"
- return 1
- fi
-
- log "LiteSpeed successfully restarted"
- return 0
}
-# Parse input parameters
-declare -a DOMAINS
+# === Main Script Logic ===
+log "Starting SSL Manager V2.0.1..."
+
+# Ensure log directory exists
+sudo mkdir -p "$LOG_DIR" || { log "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
+sudo touch "$SCRIPT_LOG" "$XML_ERROR_LOG"
+sudo chown "$(whoami)":"$(id -gn)" "$SCRIPT_LOG" "$XML_ERROR_LOG" 2>/dev/null || log_verbose "Info: Could not change ownership of log files (may require sudo)."
+
+# Argument Parsing
for arg in "$@"; do
case $arg in
--public-ip=*)
@@ -650,102 +222,66 @@ for arg in "$@"; do
--email=*)
EMAIL="${arg#*=}"
;;
+ --verbose)
+ VERBOSE=1
+ log "Verbose mode enabled."
+ ;;
*)
- echo "Invalid argument: $arg"
+ log "Invalid argument: $arg"
exit 1
;;
esac
done
-# Input validation
-log "Validating inputs..."
-if [[ -z "${PUBLIC_IP:-}" || ${#DOMAINS[@]} -eq 0 ]]; then
- echo "Error: --public-ip and --domain(s) are mandatory."
+# Input Validation
+if [[ -z "$PRIMARY_DOMAIN" || -z "$PUBLIC_IP" || -z "$EMAIL" ]]; then
+ log "❌ ERROR: Missing required parameters. Provide --domains, --public-ip, and --email."
exit 1
fi
-validate_ip "$PUBLIC_IP" || { echo "Invalid public IP: $PUBLIC_IP"; exit 1; }
-for domain in "${DOMAINS[@]}"; do
- validate_domain "$domain" || { echo "Invalid domain: $domain"; exit 1; }
-done
-if [[ -n "${EMAIL:-}" ]]; then
- validate_email "$EMAIL" || { echo "Invalid email: $EMAIL"; exit 1; }
+
+if ! validate_domain "$PRIMARY_DOMAIN"; then
+ log "❌ ERROR: Invalid domain '$PRIMARY_DOMAIN'."
+ exit 1
fi
-# Main execution loop
-for DOMAIN in "${DOMAINS[@]}"; do
- log "Processing domain: $DOMAIN"
+if ! validate_ip "$PUBLIC_IP"; then
+ log "❌ ERROR: Invalid IP address '$PUBLIC_IP'."
+ exit 1
+fi
- # Validate the domain connection
- validate_domain_connection
+if ! validate_email "$EMAIL"; then
+ log "❌ ERROR: Invalid email address '$EMAIL'."
+ exit 1
+fi
- # Install Certbot
- log "Installing Certbot..."
- if ! command -v certbot > /dev/null; then
- if [[ -f /etc/debian_version ]]; then
- apt-get update && apt-get install -y certbot
- elif [[ -f /etc/redhat-release ]]; then
- # Check if it's AlmaLinux or other RHEL derivatives
- if grep -q "AlmaLinux" /etc/os-release; then
- log "Detected AlmaLinux. Installing EPEL repository and Certbot..."
- # Install EPEL repository first
- dnf install -y epel-release
- # Install Certbot and Python modules for the webroot plugin
- dnf install -y certbot python3-certbot-apache
- else
- # Fallback for other RHEL-based systems
- yum install -y certbot
- fi
- else
- echo "Unsupported OS. Install Certbot manually."
- exit 1
- fi
- fi
+# Validate DNS and HTTP Access
+if ! validate_dns "$PRIMARY_DOMAIN" "$PUBLIC_IP"; then
+ log "❌ ERROR: DNS validation failed. Exiting."
+ exit 1
+fi
- # Check for existing certificate before requesting
- if [[ -d "/etc/letsencrypt/live/$DOMAIN" ]]; then
- log "Certificate for $DOMAIN already exists. Checking expiry..."
- EXPIRY=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/$DOMAIN/cert.pem" | cut -d= -f2)
- EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
- NOW_EPOCH=$(date +%s)
- DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
-
- if [[ $DAYS_LEFT -gt 30 ]]; then
- log "Certificate still valid for $DAYS_LEFT days. Skipping renewal."
- update_litespeed_config
- setup_cron_job
- continue
- else
- log "Certificate expires in $DAYS_LEFT days. Proceeding with renewal."
- fi
- fi
+if ! validate_http_access "$PRIMARY_DOMAIN"; then
+ log "❌ ERROR: HTTP access validation failed. Exiting."
+ exit 1
+fi
- # Modify Certbot command to include all domains
- CERTBOT_CMD="certbot certonly --webroot -w /var/www/webroot/ROOT"
- for domain in "${DOMAINS[@]}"; do
- CERTBOT_CMD+=" -d $domain"
- done
- CERTBOT_CMD+=" --agree-tos --non-interactive"
- [[ -n "${EMAIL:-}" ]] && CERTBOT_CMD+=" --email $EMAIL"
+# Dependency Checks
+log_verbose "Checking dependencies..."
+check_command "xmllint" "libxml2-utils"
+check_command "xmlstarlet" "xmlstarlet"
+check_command "certbot" "certbot"
- # After Certbot installation and before existing certificate check
- install_xml_tools
+# Create Default Backup
+create_default_backup
- # Replace the simple reload with the improved function
- if $CERTBOT_CMD; then
- log "SSL certificate issued successfully for $DOMAIN."
-
- # Update LiteSpeed config with enhanced safety
- if update_litespeed_config; then
- restart_litespeed
- send_email "$DOMAIN SSL Certificate Issued Successfully" "The SSL certificate for $DOMAIN has been successfully installed."
- setup_cron_job
- else
- log "ERROR: Failed to update LiteSpeed configuration. Manually check your configuration."
- send_email "SSL Certificate Installation Warning" "The SSL certificate for $DOMAIN was issued successfully, but there was an error updating the LiteSpeed configuration. Please check your server configuration manually."
- fi
- else
- log "Certbot failed."
- send_email "SSL Certificate Installation Failed" "An error occurred while installing the SSL certificate for $DOMAIN."
- exit 1
- fi
-done
+# Issue SSL Certificate
+issue_certificate "$PRIMARY_DOMAIN" "$EMAIL"
+
+# Update httpd_config.xml
+update_httpd_config "$PRIMARY_DOMAIN" "$PUBLIC_IP"
+
+# Restart LiteSpeed
+restart_litespeed
+
+log "✅ SSL Manager completed successfully. Certificate issued and configuration updated."
+exit 0
\ No newline at end of file
diff --git a/scripts/ssl-manager/xmlchecker.sh b/scripts/ssl-manager/xmlchecker.sh
new file mode 100644
index 0000000..dbc41f4
--- /dev/null
+++ b/scripts/ssl-manager/xmlchecker.sh
@@ -0,0 +1,375 @@
+#!/bin/bash
+# ==============================================================================
+# Script Name: xmlchecker.sh
+# Description: Validates and attempts to fix structural errors in LiteSpeed's
+# httpd_config.xml file. Includes robust fallback mechanisms,
+# semantic validation, and service restart verification.
+# Version: 1.8.0 (Enterprise-Grade Optimized)
+# Author: Anthony Garces (tony@mightybox.io)
+# Date: 2025-03-26
+# Exit Codes:
+# 0: Success (original valid or minor fixes applied, or default/backup applied successfully)
+# 1: General Error / Fallback to Backup completed (review recommended) / Restart failed
+# 2: Success, but MINIMAL fallback config applied (CRITICAL REVIEW NEEDED)
+# 3: Success, but RECOVERED config with MAJOR changes applied (CRITICAL REVIEW NEEDED)
+# ==============================================================================
+set -euo pipefail
+
+# === Configuration ===
+CONF_FILE="/var/www/conf/httpd_config.xml"
+DEFAULT_CONF="/var/www/conf/httpd_config.default.xml" # Official default file path
+SERVER_ROOT="/var/www" # Server root directory
+LOG_DIR="/var/log/mb-ssl" # Log directory
+MAJOR_CHANGE_THRESHOLD=20 # Lines changed to trigger "major recovery" warning
+
+# - Internal Variables -
+TMP_FILE="/tmp/test_config_$(date +%s).xml" # Temporary copy of original/cleaned file
+RECOVERY_TMP_FILE="/tmp/recovery_test_config_$(date +%s).xml" # Holds output of xmllint --recover
+MINIMAL_TMP="/tmp/minimal_config_$(date +%s).xml" # Temporary minimal config file
+BACKUP_FILE="${LOG_DIR}/httpd_config_$(date +%Y%m%d_%H%M%S).bak" # Timestamped backup
+XML_ERROR_LOG="${LOG_DIR}/xml-parse-error.log"
+SCRIPT_LOG="${LOG_DIR}/config_fixer.log"
+VERBOSE=0
+INITIAL_FILE_VALID=0 # Flag: 1 if CONF_FILE was valid before fixes
+RECOVERY_PRODUCED_VALID_XML=0 # Flag: 1 if xmllint --recover output was valid XML
+RECOVERY_WAS_MAJOR=0 # Flag: 1 if recovery resulted in significant changes
+APPLIED_CONFIG_SOURCE="none" # Tracks what was finally applied
+
+# === Cleanup Trap ===
+trap 'log "Script interrupted. Cleaning up temporary files..."; sudo rm -f "$TMP_FILE" "$RECOVERY_TMP_FILE" "$MINIMAL_TMP"' EXIT
+
+# === Functions ===
+log() {
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | sudo tee -a "$SCRIPT_LOG"
+}
+
+log_verbose() {
+ [[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
+}
+
+check_command() {
+ local cmd="$1"
+ local apt_pkg="$2"
+ if ! command -v "$cmd" &>/dev/null; then
+ log "⚠ Required command '$cmd' not found. Attempting to install package '$apt_pkg'..."
+ if sudo yum install -y "$apt_pkg"; then
+ log "✔ Successfully installed '$apt_pkg'."
+ else
+ log "❌ ERROR: Failed to install '$apt_pkg'. Exiting."
+ exit 1
+ fi
+ fi
+}
+
+perform_semantic_checks() {
+ local file_to_check="$1"
+ log_verbose "Performing semantic checks on '$file_to_check'..."
+ local semantic_errors=0
+
+ # Check for critical elements
+ if ! sudo xmlstarlet sel -t -c "//virtualHostList" "$file_to_check" &>/dev/null; then
+ log "Warning: is missing in '$file_to_check'."
+ semantic_errors=$((semantic_errors + 1))
+ fi
+ if ! sudo xmlstarlet sel -t -c "//listenerList" "$file_to_check" &>/dev/null; then
+ log "Warning: is missing in '$file_to_check'."
+ semantic_errors=$((semantic_errors + 1))
+ fi
+
+ # Validate listener certificates
+ local listeners
+ listeners=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener/name" "$file_to_check" 2>/dev/null || echo "")
+ for listener in $listeners; do
+ local key_file=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener[name='$listener']/keyFile" "$file_to_check" 2>/dev/null || echo "")
+ local cert_file=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener[name='$listener']/certFile" "$file_to_check" 2>/dev/null || echo "")
+
+ # Resolve paths
+ key_file=$(echo "$key_file" | sed "s|\$SERVER_ROOT|$SERVER_ROOT|g")
+ cert_file=$(echo "$cert_file" | sed "s|\$SERVER_ROOT|$SERVER_ROOT|g")
+
+ if [[ -n "$key_file" && ! -e "$key_file" ]]; then
+ log "Warning: Listener '$listener' has invalid keyFile path: '$key_file'"
+ semantic_errors=$((semantic_errors + 1))
+ fi
+ if [[ -n "$cert_file" && ! -e "$cert_file" ]]; then
+ log "Warning: Listener '$listener' has invalid certFile path: '$cert_file'"
+ semantic_errors=$((semantic_errors + 1))
+ fi
+ done
+
+ # Return result
+ if [[ "$semantic_errors" -eq 0 ]]; then
+ log_verbose "Semantic checks passed for '$file_to_check'."
+ return 0
+ else
+ log "⚠ Semantic Check: Found $semantic_errors potential issue(s) in '$file_to_check'. Review warnings."
+ return 1
+ fi
+}
+
+clean_invalid_entries() {
+ local file_to_clean="$1"
+ log "Attempting to clean invalid entries from '$file_to_clean'..."
+
+ # Remove empty or redundant nodes
+ sudo xmlstarlet ed -L -d '//virtualHost[not(node()[not(self::comment() or (self::text() and normalize-space()=""))])]' "$file_to_clean" || log "Warning: Failed to clean empty virtualHost nodes."
+ sudo xmlstarlet ed -L -d '//listener[not(node()[not(self::comment() or (self::text() and normalize-space()=""))])]' "$file_to_clean" || log "Warning: Failed to clean empty listener nodes."
+ sudo xmlstarlet ed -L -d '//virtualHostList[not(node()[not(self::comment() or (self::text() and normalize-space()=""))])]' "$file_to_clean" || log "Warning: Failed to clean empty virtualHostList."
+ sudo xmlstarlet ed -L -d '//listenerList[not(node()[not(self::comment() or (self::text() and normalize-space()=""))])]' "$file_to_clean" || log "Warning: Failed to clean empty listenerList."
+
+ log "Cleanup attempts completed for '$file_to_clean'."
+}
+
+restart_litespeed() {
+ log "Attempting to restart LiteSpeed server..."
+ if sudo systemctl restart lsws; then
+ log "Info: Restart command issued for LiteSpeed (lsws). Waiting 3 seconds to verify status..."
+ sleep 3
+ if sudo systemctl is-active --quiet lsws; then
+ log "✔ LiteSpeed (lsws) is active after restart."
+ return 0
+ else
+ log "❌ FATAL: LiteSpeed (lsws) failed to become active after restart command."
+ log " Check service status: 'sudo systemctl status lsws'"
+ log " Check LiteSpeed error log: '${SERVER_ROOT:-/var/www}/logs/error.log'"
+ log " Consider manually reverting to backup: sudo cp '$BACKUP_FILE' '$CONF_FILE' && sudo systemctl restart lsws"
+ return 1
+ fi
+ else
+ log "❌ FATAL: Failed to execute restart command for LiteSpeed (lsws)."
+ return 1
+ fi
+}
+
+apply_and_restart() {
+ local source_file="$1"
+ local source_desc="$2" # e.g., "original", "recovered", "default", "backup", "minimal"
+
+ APPLIED_CONFIG_SOURCE="$source_desc" # Update global tracker
+
+ log "Applying changes from $source_desc file ('$source_file') to '$CONF_FILE'..."
+
+ # Check if source and destination are the same file
+ if [[ "$source_file" == "$CONF_FILE" ]]; then
+ log "Info: Source file ('$source_file') and destination file ('$CONF_FILE') are the same. Skipping copy."
+ else
+ log "Preview of changes (diff with backup '$BACKUP_FILE'):"
+ if ! sudo diff -u "$BACKUP_FILE" "$source_file" | sudo tee -a "$SCRIPT_LOG"; then
+ log "Info: No differences found or diff command failed."
+ fi
+
+ # Apply the configuration using sudo cp
+ if sudo cp -a "$source_file" "$CONF_FILE"; then
+ log "✔ Configuration updated successfully from $source_desc file."
+ else
+ log "❌ FATAL: Failed to copy $source_desc file '$source_file' to '$CONF_FILE'. Configuration NOT updated."
+ return 1 # Signal failure
+ fi
+ fi
+
+ # Restart LiteSpeed using the hardcoded command
+ log "Attempting to restart LiteSpeed server..."
+ if sudo systemctl restart lsws; then
+ log "Info: Restart command issued for LiteSpeed (lsws). Waiting 3 seconds to verify status..."
+ sleep 3
+ if sudo systemctl is-active --quiet lsws; then
+ log "✔ LiteSpeed (lsws) is active after restart."
+ return 0 # Success
+ else
+ log "❌ FATAL: LiteSpeed (lsws) failed to become active after restart command."
+ log " Check service status: 'sudo systemctl status lsws'"
+ log " Check LiteSpeed error log: '${SERVER_ROOT:-/var/www}/logs/error.log'"
+ log " Consider manually reverting to backup: sudo cp '$BACKUP_FILE' '$CONF_FILE' && sudo systemctl restart lsws"
+ return 1 # Failure
+ fi
+ else
+ log "❌ FATAL: Failed to execute restart command for LiteSpeed (lsws)."
+ return 1 # Failure
+ fi
+}
+
+# === Main Script Logic ===
+log "Starting XML config check/fix V1.8.0 for $CONF_FILE"
+sudo mkdir -p "$LOG_DIR" || { log "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
+sudo touch "$SCRIPT_LOG" "$XML_ERROR_LOG"
+sudo chown "$(whoami)":"$(id -gn)" "$SCRIPT_LOG" "$XML_ERROR_LOG" 2>/dev/null || log_verbose "Info: Could not change ownership of log files (may require sudo)."
+
+# Argument Parsing
+if [[ "${1:-}" == "--verbose" ]]; then
+ VERBOSE=1
+ log "Verbose mode enabled."
+fi
+
+# Dependency Checks
+log_verbose "Checking dependencies..."
+check_command "xmllint" "libxml2-utils"
+check_command "xmlstarlet" "xmlstarlet"
+
+# Backup Original File
+log "Backing up '$CONF_FILE' to '$BACKUP_FILE'..."
+sudo cp -a "$CONF_FILE" "$BACKUP_FILE"
+
+# Initial Validation
+log_verbose "Running initial xmllint validation on '$CONF_FILE'..."
+if sudo xmllint --noout "$CONF_FILE" 2>"$XML_ERROR_LOG"; then
+ log "✔ Initial Check: '$CONF_FILE' is valid XML."
+ INITIAL_FILE_VALID=1
+else
+ log "⚠ Initial Check: '$CONF_FILE' is invalid XML. Will attempt recovery/cleanup."
+fi
+
+# Recovery Attempt
+if [[ "$INITIAL_FILE_VALID" -eq 0 ]]; then
+ log "Attempting automatic recovery with 'xmllint --recover'..."
+ sudo rm -f "$RECOVERY_TMP_FILE"
+ if xmllint --recover "$CONF_FILE" 2>/dev/null > "$RECOVERY_TMP_FILE"; then
+ if sudo xmllint --noout "$RECOVERY_TMP_FILE" 2>/dev/null; then
+ log "✔ 'xmllint --recover' produced structurally valid XML output."
+ RECOVERY_PRODUCED_VALID_XML=1
+ else
+ log "⚠ 'xmllint --recover' ran but the output is STILL invalid XML. Discarding recovery."
+ sudo rm -f "$RECOVERY_TMP_FILE"
+ fi
+ else
+ log "⚠ 'xmllint --recover' command failed or produced errors. Discarding recovery."
+ sudo rm -f "$RECOVERY_TMP_FILE"
+ fi
+fi
+
+# Final Validation and Decision
+FINAL_CONFIG_SOURCE_FILE=""
+FINAL_CONFIG_SOURCE_DESC=""
+FINAL_EXIT_CODE=0
+log_verbose "Running final validation and decision logic..."
+
+# Check 1: Is the original (potentially cleaned) temp file valid now?
+if [[ "$INITIAL_FILE_VALID" -eq 1 ]]; then
+ FINAL_CONFIG_SOURCE_FILE="$CONF_FILE"
+ FINAL_CONFIG_SOURCE_DESC="original"
+elif [[ "$RECOVERY_PRODUCED_VALID_XML" -eq 1 ]]; then
+ FINAL_CONFIG_SOURCE_FILE="$RECOVERY_TMP_FILE"
+ FINAL_CONFIG_SOURCE_DESC="recovered"
+else
+ log "❌ FATAL: Neither original nor recovered file is valid. Falling back to default or minimal config."
+ FINAL_EXIT_CODE=2
+fi
+
+# Apply and Restart
+if [[ -n "$FINAL_CONFIG_SOURCE_FILE" ]]; then
+ if apply_and_restart "$FINAL_CONFIG_SOURCE_FILE" "$FINAL_CONFIG_SOURCE_DESC"; then
+ log "✅ Script completed successfully. Applied: $FINAL_CONFIG_SOURCE_DESC."
+ exit "$FINAL_EXIT_CODE"
+ else
+ log "❌ FATAL: Failed to apply and restart using $FINAL_CONFIG_SOURCE_DESC config."
+ exit 1
+ fi
+else
+ log "❌ FATAL: No valid configuration file found. Exiting."
+ exit 1
+fi
+
+# === Fallback Logic ===
+log "Attempting fallback sequence..."
+FALLBACK_APPLIED="" # Track which fallback succeeded
+
+# Priority 1: Official Default Config (with include checks)
+if [[ -z "$FALLBACK_APPLIED" && -f "$DEFAULT_CONF" ]]; then
+ log_verbose "Fallback Priority 1: Checking official default config '$DEFAULT_CONF'..."
+ if sudo xmllint --noout "$DEFAULT_CONF" 2>/dev/null; then
+ # Validate semantic correctness of the default config
+ if perform_semantic_checks "$DEFAULT_CONF"; then
+ log "Info: Default config is valid. Applying default configuration."
+
+ # Log the exact content of the default config for debugging
+ log_verbose "Exact content of default config ('$DEFAULT_CONF'):"
+ sudo cat "$DEFAULT_CONF" | sudo tee -a "$SCRIPT_LOG" >/dev/null
+
+ # Apply the default configuration
+ if apply_and_restart "$DEFAULT_CONF" "default"; then
+ log "✅ Script completed successfully. Applied: default configuration."
+
+ # Verify the copied file matches the default config exactly
+ if ! sudo diff -q "$DEFAULT_CONF" "$CONF_FILE" &>/dev/null; then
+ log "❌ FATAL: Copied file does NOT match the default configuration exactly!"
+ log " Differences detected between '$DEFAULT_CONF' and '$CONF_FILE'."
+ exit 1
+ else
+ log "✔ Verified: Copied file matches the default configuration exactly."
+ fi
+
+ FALLBACK_APPLIED="default"
+ exit 0 # Exit code 0 for success with default config
+ else
+ log "❌ FATAL: Failed to apply/restart using default configuration!"
+ fi
+ else
+ log "⚠ Semantic Check: Default config failed semantic validation. Skipping fallback to default."
+ fi
+ else
+ log "⚠ Structural Check: Default config failed XML validation. Skipping fallback to default."
+ fi
+fi
+
+# Priority 2: Minimal Fallback Configuration
+if [[ -z "$FALLBACK_APPLIED" ]]; then
+ log "Fallback Priority 2: Generating minimal fallback configuration..."
+ cat </dev/null
+
+
+ LiteSpeed
+
+
+ HTTP
+ *:80
+
+
+
+
+ default
+ /var/www/webroot/
+ \$SERVER_ROOT/conf/vhconf.xml
+ 1
+ 1
+ 1
+ 0
+ 0
+
+
+
+
+ lsapi
+ lsphp
+ uds://tmp/lshttpd/lsphp.sock
+ 35
+ PHP_LSAPI_MAX_REQUESTS=500
+ 60
+ 0
+ 1
+
+
+
+EOF
+
+ # Validate minimal config
+ if sudo xmllint --noout "$MINIMAL_TMP" 2>/dev/null; then
+ if perform_semantic_checks "$MINIMAL_TMP"; then
+ log "Info: Minimal fallback config generated successfully and passed validation."
+ if apply_and_restart "$MINIMAL_TMP" "minimal"; then
+ log "🚨 CRITICAL WARNING: Server running on MINIMAL config. Manual reconfiguration required! 🚨"
+ log "✅ Script completed. Applied: minimal configuration."
+ FALLBACK_APPLIED="minimal"
+ exit 2 # Exit code 2 for minimal config
+ else
+ log "❌ FATAL: Failed to apply/restart even the minimal config!"
+ fi
+ else
+ log "❌ FATAL: Generated minimal config failed semantic validation! This is a script bug."
+ fi
+ else
+ log "❌ FATAL: Generated minimal config is invalid XML! This is a script bug."
+ fi
+fi
+
+# If we reach here, all fallbacks failed
+log "❌ FATAL: All fallback mechanisms failed. Server configuration remains unchanged."
+exit 1
\ No newline at end of file