375 lines
16 KiB
Bash
375 lines
16 KiB
Bash
#!/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: <virtualHostList> 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: <listenerList> 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 <<EOF | sudo tee "$MINIMAL_TMP" >/dev/null
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<httpServerConfig>
|
|
<serverName>LiteSpeed</serverName>
|
|
<listenerList>
|
|
<listener>
|
|
<name>HTTP</name>
|
|
<address>*:80</address>
|
|
</listener>
|
|
</listenerList>
|
|
<virtualHostList>
|
|
<virtualHost>
|
|
<name>default</name>
|
|
<vhRoot>/var/www/webroot/</vhRoot>
|
|
<configFile>\$SERVER_ROOT/conf/vhconf.xml</configFile>
|
|
<allowSymbolLink>1</allowSymbolLink>
|
|
<enableScript>1</enableScript>
|
|
<restrained>1</restrained>
|
|
<setUIDMode>0</setUIDMode>
|
|
<chrootMode>0</chrootMode>
|
|
</virtualHost>
|
|
</virtualHostList>
|
|
<extProcessorList>
|
|
<extProcessor>
|
|
<type>lsapi</type>
|
|
<name>lsphp</name>
|
|
<address>uds://tmp/lshttpd/lsphp.sock</address>
|
|
<maxConns>35</maxConns>
|
|
<env>PHP_LSAPI_MAX_REQUESTS=500</env>
|
|
<initTimeout>60</initTimeout>
|
|
<retryTimeout>0</retryTimeout>
|
|
<persistConn>1</persistConn>
|
|
</extProcessor>
|
|
</extProcessorList>
|
|
</httpServerConfig>
|
|
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 |