Added XMLChecker and also optimized SSL Manager
parent
7a16665ce8
commit
5653d318ba
22
mbadmin.jps
22
mbadmin.jps
|
@ -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
|
||||
|
|
|
@ -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:-}"
|
||||
log_verbose() {
|
||||
[[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
|
||||
}
|
||||
|
||||
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."
|
||||
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 "Email not provided. Skipping email notification."
|
||||
log "❌ ERROR: Failed to install '$apt_pkg'. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 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."
|
||||
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"
|
||||
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
|
||||
|
||||
log "Domain-specific HTTPS listener for $domain created successfully."
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to create or update domain-specific virtual host
|
||||
create_domain_virtual_host() {
|
||||
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."
|
||||
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."
|
||||
validate_ip() {
|
||||
local ip="$1"
|
||||
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# Revised service restart with pre-check
|
||||
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"
|
||||
|
||||
# Validate the domain connection
|
||||
validate_domain_connection
|
||||
|
||||
# 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."
|
||||
if ! validate_ip "$PUBLIC_IP"; then
|
||||
log "❌ ERROR: Invalid IP address '$PUBLIC_IP'."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
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
|
||||
|
||||
# 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"
|
||||
|
||||
# After Certbot installation and before existing certificate check
|
||||
install_xml_tools
|
||||
|
||||
# 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."
|
||||
if ! validate_email "$EMAIL"; then
|
||||
log "❌ ERROR: Invalid email address '$EMAIL'."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Validate DNS and HTTP Access
|
||||
if ! validate_dns "$PRIMARY_DOMAIN" "$PUBLIC_IP"; then
|
||||
log "❌ ERROR: DNS validation failed. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! validate_http_access "$PRIMARY_DOMAIN"; then
|
||||
log "❌ ERROR: HTTP access validation failed. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Dependency Checks
|
||||
log_verbose "Checking dependencies..."
|
||||
check_command "xmllint" "libxml2-utils"
|
||||
check_command "xmlstarlet" "xmlstarlet"
|
||||
check_command "certbot" "certbot"
|
||||
|
||||
# Create Default Backup
|
||||
create_default_backup
|
||||
|
||||
# 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
|
|
@ -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
|
Loading…
Reference in New Issue