Compare commits

..

2 Commits

Author SHA1 Message Date
Anthony a0feb8d70d Optimized SSL Remover Shell SCript 2025-03-27 22:42:08 +08:00
Anthony 5653d318ba Added XMLChecker and also optimized SSL Manager 2025-03-26 02:45:01 +08:00
4 changed files with 746 additions and 779 deletions

View File

@ -617,20 +617,26 @@ actions:
CONF_FILE="/var/www/conf/httpd_config.xml"
echo "Analyzing LiteSpeed configuration tags..."
echo "-----------------------------------"
grep -c '<n>' "${CONF_FILE}" | { echo "Number of <n> tags: $(cat)"; }
grep -c '</n>' "${CONF_FILE}" | { echo "Number of </n> tags: $(cat)"; }
grep -c '<name>' "${CONF_FILE}" | { echo "Number of <name> tags: $(cat)"; }
grep -c '</name>' "${CONF_FILE}" | { echo "Number of </name> tags: $(cat)"; }
num_n=$(grep -c '<n>' "${CONF_FILE}")
num_n_close=$(grep -c '</n>' "${CONF_FILE}")
num_name=$(grep -c '<name>' "${CONF_FILE}")
num_name_close=$(grep -c '</name>' "${CONF_FILE}")
echo "Number of <n> tags: $num_n"
echo "Number of </n> tags: $num_n_close"
echo "Number of <name> tags: $num_name"
echo "Number of </name> tags: $num_name_close"
echo "-----------------------------------"
echo "First 5 instances of <n> tags:"
grep -n '<n>' "${CONF_FILE}" | head -5
echo "-----------------------------------"
echo "Testing sed command effectiveness:"
cp "${CONF_FILE}" /tmp/test_config.xml
sed -i 's|<n>|<name>|g' /tmp/test_config.xml
sed -i 's|</n>|</name>|g' /tmp/test_config.xml
echo "After sed, remaining <n> tags: $(grep -c '<n>' /tmp/test_config.xml)"
echo "After sed, remaining </n> tags: $(grep -c '</n>' /tmp/test_config.xml)"
sed -i 's#<n>#<name>#g' /tmp/test_config.xml
sed -i 's#</n>#</name>#g' /tmp/test_config.xml
new_num_n=$(grep -c '<n>' /tmp/test_config.xml)
new_num_n_close=$(grep -c '</n>' /tmp/test_config.xml)
echo "After sed, remaining <n> tags: $new_num_n"
echo "After sed, remaining </n> tags: $new_num_n_close"
echo "-----------------------------------"
- return:
type: info

View File

@ -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 '<listener>' "$config_file")
local close_listeners=$(grep -c '</listener>' "$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 <listener> 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 "<listener>" | 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 "</listener>" | 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 "<!-- Redundant listeners for $domain removed by ssl_manager.sh -->" >> "$temp_file"
# Verify the result isn't empty or corrupted
if [ -s "$temp_file" ] && grep -q "<httpServerConfig>" "$temp_file" && grep -q "</httpServerConfig>" "$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="<name>$listener_name</name>"
local map_pattern="<vhostMapList>"
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 "<name>$listener_name</name>" "$config_file" | grep -q "<domain>$DOMAIN</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<\/domain>/,/<\/vhostMap>/s/<vhost>Jelastic<\/vhost>/<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 " <vhostMap>"
print " <vhost>" vhost "</vhost>"
print " <domain>" domain "</domain>"
print " </vhostMap>"
}
}
' "$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 "<vhostMapList>" "$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 "<name>$listener_name</name>" "$config_file" | grep -q "<domain>$DOMAIN</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 "<name>HTTPS-$domain</name>" "$config_file"; then
log "Updating existing listener for $domain..."
# Use full XML scope for replacements
sed -i "/<name>HTTPS-$domain<\/name>/,/<\/listener>/ {
s|<keyFile>.*</keyFile>|<keyFile>$key_file</keyFile>|;
s|<certFile>.*</certFile>|<certFile>$cert_file</certFile>|;
}" "$config_file"
return 0
fi
log "Creating new HTTPS listener for $domain..."
# Generate properly indented XML block
listener_xml=$(cat <<EOF
<listener>
<name>HTTPS-${domain}</name>
<address>*:443</address>
<secure>1</secure>
<vhostMapList>
<vhostMap>
<vhost>${vhost_name}</vhost>
<domain>${domain}</domain>
</vhostMap>
</vhostMapList>
<keyFile>${key_file}</keyFile>
<certFile>${cert_file}</certFile>
<sslProtocol>24</sslProtocol>
<ciphers>ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384</ciphers>
</listener>
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 "<name>$vhost_name</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 " <virtualHost>"
print " <name>" vhost "</name>"
print " <vhRoot>/var/www/webroot/</vhRoot>"
print " <configFile>$SERVER_ROOT/conf/vhconf.xml</configFile>"
print " <allowSymbolLink>1</allowSymbolLink>"
print " <enableScript>1</enableScript>"
print " <restrained>1</restrained>"
print " <setUIDMode>0</setUIDMode>"
print " <chrootMode>0</chrootMode>"
print " </virtualHost>"
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" '
/<name>HTTPS<\/name>/,/<\/listener>/ {
if ($0 ~ /<vhostMap>/) {
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" '
/<name>HTTPS-ipv6<\/name>/,/<\/listener>/ {
if ($0 ~ /<vhostMap>/) {
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

View File

@ -1,25 +1,42 @@
#!/bin/bash
# ==============================================================================
# Script Name: ssl_remover.sh
# Description: Removes SSL certificates and cleans up LiteSpeed configurations.
# Ensures safe removal of listeners, virtual hosts, and certificates.
# Version: 2.0.0 (Professional-Grade Optimized)
# Author: Gemini (Based on user request and feedback)
# Date: 2025-03-26
# Exit Codes:
# 0: Success (SSL removed and configuration cleaned up)
# 1: General Error (e.g., invalid parameters, XML validation failed)
# 2: Backup/Restore Error (e.g., failed to create 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"
BACKUP_DIR="/var/www/conf/backups"
LOG_DIR="/var/log/mb-ssl"
LOG_FILE="$LOG_DIR/ssl-remover.log"
mkdir -p "$LOG_DIR"
chmod 0755 "$LOG_DIR"
exec > >(tee -a "$LOG_FILE") 2>&1
CERT_DIR="/etc/letsencrypt/live"
SCRIPT_LOG="${LOG_DIR}/ssl-remover.log"
VERBOSE=0
# Function to log messages
# === Functions ===
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$SCRIPT_LOG"
}
log_verbose() {
[[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
}
# Email function (same as in ssl_manager.sh)
send_email() {
local subject="$1"
local body="$2"
local recipient="${EMAIL:-}"
[[ -n "$recipient" ]] && {
if [[ -n "$recipient" ]]; then
log "Sending email notification to $recipient..."
curl -s "https://api.postmarkapp.com/email" \
-X POST \
@ -33,121 +50,153 @@ send_email() {
\"HtmlBody\": \"$body\",
\"MessageStream\": \"outbound\"
}" > /dev/null && log "Email sent." || log "Email failed."
}
fi
}
validate_domain() {
local domain="$1"
if [[ "$domain" =~ ^([a-zA-Z0-9](-*[a-zA-Z0-9])*\.)+[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
# Backup configuration with timestamp
backup_config() {
local config_file="/var/www/conf/httpd_config.xml"
local backup_dir="/var/www/conf/backups"
local timestamp=$(date +%Y%m%d%H%M%S)
mkdir -p "$backup_dir"
cp "$config_file" "$backup_dir/httpd_config.pre-removal-$timestamp.xml"
log "Config backup saved to $backup_dir/httpd_config.pre-removal-$timestamp.xml"
local backup_file="${BACKUP_DIR}/httpd_config.pre-removal-${timestamp}.xml"
mkdir -p "$BACKUP_DIR"
if cp "$CONF_FILE" "$backup_file"; then
log "Config backup saved to $backup_file"
else
log "❌ ERROR: Failed to create backup '$backup_file'. Exiting."
exit 2
fi
}
# Remove certificate using Certbot
remove_certificate() {
local domain="$1"
log "Checking for certificate for domain '$domain'..."
if certbot certificates | grep -q "Domains: $domain"; then
log "Removing certificate for $domain..."
certbot delete --cert-name "$domain" --non-interactive
rm -rf "/etc/letsencrypt/live/$domain"*
log "Certificate removed for $domain"
else
log "No certificate found for $domain"
fi
}
# Remove listeners and associated configurations
cleanup_listeners() {
local domain="$1"
local config_file="/var/www/conf/httpd_config.xml"
local temp_file
log "Cleaning up listeners for $domain..."
# Remove listeners
sed -i "/<name>HTTPS-$domain<\/name>/,/<\/listener>/d" "$config_file"
# Remove vhostMap entries
sed -i "/<domain>$domain<\/domain>/,/<\/vhostMap>/d" "$config_file"
# Remove related virtual host
local vhost_name="${domain//./_}"
sed -i "/<name>$vhost_name<\/name>/,/<\/virtualHost>/d" "$config_file"
# Cleanup empty listenerList tags
temp_file=$(mktemp)
awk '/<listenerList>/ {flag=1; print; next} /<\/listenerList>/ {flag=0; print; next} flag && /^[[:space:]]*$/ {next} {print}' "$config_file" > "$temp_file"
mv "$temp_file" "$config_file"
}
# Validate XML configuration
validate_xml() {
local config_file="/var/www/conf/httpd_config.xml"
if command -v xmllint >/dev/null; then
log "Validating XML configuration..."
if ! xmllint --noout "$config_file"; then
log "ERROR: Invalid XML configuration after cleanup. Check backups."
log "Removing certificate for '$domain'..."
if certbot delete --cert-name "$domain" --non-interactive; then
rm -rf "/etc/letsencrypt/live/$domain"*
log "Certificate successfully removed for '$domain'."
else
log "❌ ERROR: Failed to remove certificate for '$domain'."
return 1
fi
else
log "No certificate found for '$domain'. Skipping removal."
fi
}
cleanup_listeners() {
local domain="$1"
log "Cleaning up listeners and configurations for '$domain'..."
# Remove listener for the domain
sudo xmlstarlet ed -L \
-d "//listener[name='HTTPS-$domain']" \
"$CONF_FILE"
# Remove vhostMap entries
sudo xmlstarlet ed -L \
-d "//vhostMap[domain='$domain']" \
"$CONF_FILE"
# Remove related virtual host
local vhost_name="${domain//./_}"
sudo xmlstarlet ed -L \
-d "//virtualHost[name='$vhost_name']" \
"$CONF_FILE"
# Cleanup empty listenerList tags
sudo xmlstarlet ed -L \
-d "//listenerList[count(*)=0]" \
"$CONF_FILE"
}
validate_xml() {
log "Validating XML configuration..."
if ! sudo xmllint --noout "$CONF_FILE" 2>/dev/null; then
log "❌ ERROR: Invalid XML configuration after cleanup. Check backups."
return 1
fi
log "✔ XML configuration is valid."
return 0
}
# Restart LiteSpeed if needed
restart_litespeed() {
log "Restarting LiteSpeed..."
systemctl restart lsws && log "LiteSpeed restarted successfully." || log "LiteSpeed restart failed."
log "Restarting LiteSpeed server..."
if sudo systemctl restart lsws; then
log "✔ LiteSpeed server restarted successfully."
else
log "❌ ERROR: Failed to restart LiteSpeed server."
return 3
fi
}
# Main execution
# === Main Script Logic ===
main() {
declare -a DOMAINS
EMAIL=""
# Parse parameters
while [[ $# -gt 0 ]]; do
case "$1" in
--domains=*)
IFS=',' read -ra DOMAINS <<< "${1#*=}"
shift
;;
--email=*)
EMAIL="${1#*=}"
shift
;;
--verbose)
VERBOSE=1
log "Verbose mode enabled."
;;
*)
echo "Invalid parameter: $1"
log "Invalid parameter: $1"
exit 1
;;
esac
shift
done
# Validate input
if [[ ${#DOMAINS[@]} -eq 0 ]]; then
echo "Error: --domains parameter is required"
log "❌ ERROR: --domains parameter is required."
exit 1
fi
# Ensure log directory exists
mkdir -p "$LOG_DIR" || { log "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
touch "$SCRIPT_LOG"
chmod 0644 "$SCRIPT_LOG"
# Backup configuration
backup_config
# Process each domain
for domain in "${DOMAINS[@]}"; do
log "Processing domain: $domain"
# Validate domain format
[[ "$domain" =~ ^([a-zA-Z0-9](-*[a-zA-Z0-9])*\.)+[a-zA-Z]{2,}$ ]] || {
log "Invalid domain: $domain"
if ! validate_domain "$domain"; then
log "❌ ERROR: Invalid domain '$domain'. Skipping."
continue
}
fi
# Remove certificate
remove_certificate "$domain"
# Clean up listeners and configurations
cleanup_listeners "$domain"
done
# Validate XML configuration
if validate_xml; then
restart_litespeed
send_email "SSL Removal Complete" "Successfully removed SSL for domains: ${DOMAINS[*]}"
@ -157,4 +206,5 @@ main() {
fi
}
main "$@"
# === Entry Point ===
main "$@"

View File

@ -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: <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