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