From 5653d318babbae8f1d77c2015d50afe2b306f88a Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 26 Mar 2025 02:45:01 +0800 Subject: [PATCH] Added XMLChecker and also optimized SSL Manager --- mbadmin.jps | 22 +- scripts/ssl-manager/ssl_manager.sh | 924 +++++++---------------------- scripts/ssl-manager/xmlchecker.sh | 375 ++++++++++++ 3 files changed, 619 insertions(+), 702 deletions(-) create mode 100644 scripts/ssl-manager/xmlchecker.sh 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