diff --git a/scripts/ssl-manager/ssl_manager.sh b/scripts/ssl-manager/ssl_manager.sh index 229190c..338c739 100644 --- a/scripts/ssl-manager/ssl_manager.sh +++ b/scripts/ssl-manager/ssl_manager.sh @@ -108,30 +108,67 @@ validate_domain_connection() { fi } -# Function to update LiteSpeed configuration +# 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..." - if grep -q "$DOMAIN" "$config_file"; then - log "Domain $DOMAIN already exists in the configuration. Updating..." - sed -i "//,/<\/listener>/s|.*|$key_file|" "$config_file" - sed -i "//,/<\/listener>/s|.*|$cert_file|" "$config_file" + + # Ensure we have a backup with timestamp + cp "$config_file" "$backup_file" + log "Created backup of LiteSpeed configuration at $backup_file" + + # First, clean up any redundant listeners for this domain + cleanup_redundant_listeners "$config_file" "$DOMAIN" + + # After cleaning up redundant listeners but before adding domain mappings, + # create a virtual host for the domain + create_domain_virtual_host "$DOMAIN" + + # Check if the required listeners exist and contain correct cert paths + local need_cert_update=false + local has_domain_listener=false + + # Check if domain exists in HTTPS and HTTPS-ipv6 listeners with correct cert paths + if grep -A30 "HTTPS" "$config_file" | grep -q "$DOMAIN" && \ + grep -A30 "HTTPS-ipv6" "$config_file" | grep -q "$DOMAIN"; then + # Domain exists in both listeners, check cert paths + if ! grep -A30 "HTTPS" "$config_file" | grep -q "$key_file" || \ + ! grep -A30 "HTTPS-ipv6" "$config_file" | grep -q "$key_file"; then + need_cert_update=true + log "Certificate paths need updating" + else + log "Domain mappings and certificate paths are already correct" + fi else - log "Domain $DOMAIN not found in configuration. Adding new listener..." - sed -i "/<\/listenerList>/i\ - \ - HTTPS-$DOMAIN\ -
*:443
\ - 1\ - $key_file\ - $cert_file\ - 1\ -
" "$config_file" + log "Domain mappings need to be added" fi - log "LiteSpeed configuration updated successfully." + + # Update certificate paths if needed + if [ "$need_cert_update" = true ]; then + log "Updating certificate paths..." + # Update keyFile and certFile for all listeners that match our domain + sed -i "//,/<\/listener>/ s|.*letsencrypt/live/$DOMAIN/.*|$key_file|g" "$config_file" + sed -i "//,/<\/listener>/ s|.*letsencrypt/live/$DOMAIN/.*|$cert_file|g" "$config_file" + fi + + # Add domain mapping to listeners (modified function will use the correct virtual host) + add_domain_mapping "HTTPS" + add_domain_mapping "HTTPS-ipv6" + + # 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 proper domain-to-virtualhost mapping." + return 0 } # Function to set up automatic renewal @@ -163,6 +200,336 @@ setup_cron_job() { 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 if xmllint is available + if ! command -v xmllint >/dev/null 2>&1; then + log "WARNING: xmllint not available. Skipping XML validation." + return 0 # Return success and continue + fi + + # Create a temporary validation copy (don't modify the original yet) + local validate_file=$(mktemp) + if [ ! -f "$validate_file" ]; then + log "Error: Failed to create temporary file for validation." + return 0 # Continue without validation rather than failing + fi + + # Copy the file - don't try to fix formatting + cp "$config_file" "$validate_file" + + # Try basic validation first + if xmllint --noout "$validate_file" 2>/dev/null; then + log "XML configuration validation passed." + rm -f "$validate_file" + return 0 + fi + + # Validation failed - attempt a simple check to see if main tags are balanced + local open_http=$(grep -c "" "$config_file") + local close_http=$(grep -c "" "$config_file") + local open_listeners=$(grep -c "" "$config_file") + local close_listeners=$(grep -c "" "$config_file") + + if [ "$open_http" -eq "$close_http" ] && [ "$open_listeners" -eq "$close_listeners" ]; then + log "WARNING: XML syntax validation failed but basic structure seems intact. Proceeding with caution." + rm -f "$validate_file" + return 0 # Continue anyway - LiteSpeed may be more forgiving than xmllint + fi + + # If we reach here, validation failed and basic structure check failed + log "ERROR: XML validation failed. Configuration file may be corrupted." + log "Found $open_http opening and $close_http closing httpServerConfig tags" + log "Found $open_listeners opening and $close_listeners closing listener tags" + + rm -f "$validate_file" + + if [ -f "$backup_file" ]; then + log "Restoring from backup..." + cp "$backup_file" "$config_file" + log "Backup restored. Please check your configuration manually." + fi + + return 1 +} + +# Function to clean up redundant listeners with more reliable pattern matching +cleanup_redundant_listeners() { + local config_file="$1" + local domain="$2" + + log "Checking for redundant listeners..." + + # Use grep to find the exact line numbers of redundant listeners + local line_nums=$(grep -n "HTTPS-$domain" "$config_file" | cut -d: -f1) + + if [ -n "$line_nums" ]; then + log "Found redundant listener(s) for $domain. Cleaning up..." + + # Create a temporary file + local temp_file=$(mktemp) + if [ ! -f "$temp_file" ]; then + log "Error: Failed to create temporary file for cleanup." + return 0 + fi + + # Copy the original file + cp "$config_file" "$temp_file" + + # For each match, find the enclosing tags and remove the section + for line_num in $line_nums; do + # Find the start of the listener section (search backward for opening tag) + local start_line=$(head -n "$line_num" "$config_file" | grep -n "" | tail -1 | cut -d: -f1) + if [ -z "$start_line" ]; then + continue + fi + + # Find the end of the listener section (search forward for closing tag) + local end_line=$(tail -n "+$line_num" "$config_file" | grep -n "" | head -1 | cut -d: -f1) + if [ -z "$end_line" ]; then + continue + fi + end_line=$((line_num + end_line - 1)) + + # Remove the section from the temp file + sed -i "${start_line},${end_line}d" "$temp_file" + done + + # Add a comment to indicate removal + echo "" >> "$temp_file" + + # Verify the result isn't empty or corrupted + if [ -s "$temp_file" ] && grep -q "" "$temp_file" && grep -q "" "$temp_file"; then + cp "$temp_file" "$config_file" + log "Redundant listeners successfully removed." + else + log "Error: Generated configuration is invalid. Keeping original." + fi + + rm -f "$temp_file" + else + log "No redundant listeners found." + fi + + return 0 +} + +# Function to add domain mapping to a listener if it doesn't exist - Updated to use domain-specific virtual host +add_domain_mapping() { + local listener_name="$1" + local start_pattern="$listener_name" + local map_pattern="" + local config_file="/var/www/conf/httpd_config.xml" + local vhost_name="${DOMAIN%%.*}" + + # Check if listener exists + if ! grep -q "$start_pattern" "$config_file"; then + log "Error: Listener $listener_name not found in configuration." + return 1 + fi + + # Skip if mapping already exists (safer approach) + if grep -A30 "$listener_name" "$config_file" | grep -q "$DOMAIN"; then + log "Domain mapping for $DOMAIN already exists in $listener_name listener. Updating virtual host mapping..." + # Update existing mapping to point to the correct virtual host + sed -i "/$DOMAIN<\/domain>/,/<\/vhostMap>/s/Jelastic<\/vhost>/$vhost_name<\/vhost>/g" "$config_file" + log "Updated existing domain mapping to use correct virtual host." + return 0 + fi + + log "Adding domain mapping for $DOMAIN to $listener_name listener..." + + # Create a temporary file for processing + local temp_file=$(mktemp) + if [ $? -ne 0 ]; then + log "Error: Failed to create temporary file. Skipping domain mapping for $listener_name." + return 1 + fi + + # Process the file to add domain mapping with correct virtual host + awk -v domain="$DOMAIN" -v vhost="$vhost_name" -v start="$start_pattern" -v pattern="$map_pattern" ' + { + print $0 + if ($0 ~ start) { + in_listener = 1 + } else if (in_listener && $0 ~ /<\/listener>/) { + in_listener = 0 + } else if (in_listener && $0 ~ pattern) { + print " " + print " " vhost "" + print " " domain "" + print " " + } + } + ' "$config_file" > "$temp_file" + + # Verify the processed file is valid and not empty + if [ ! -s "$temp_file" ]; then + log "Error: Generated configuration is empty. Keeping original configuration." + rm -f "$temp_file" + return 1 + fi + + # Check if the XML structure looks valid (basic check) + if ! grep -q "" "$temp_file"; then + log "Error: Generated configuration appears invalid. Keeping original configuration." + rm -f "$temp_file" + return 1 + fi + + # Compare to ensure we didn't remove anything important (line count check) + original_lines=$(wc -l < "$config_file") + new_lines=$(wc -l < "$temp_file") + if [ $new_lines -lt $(($original_lines - 5)) ]; then + log "Error: New configuration is significantly smaller than original. Aborting." + rm -f "$temp_file" + return 1 + fi + + # Replace original with processed file + cp "$temp_file" "$config_file" + if [ $? -ne 0 ]; then + log "Error: Failed to update configuration file. Keeping original configuration." + rm -f "$temp_file" + return 1 + fi + + # Clean up + rm -f "$temp_file" + + log "Domain mapping added successfully to $listener_name listener with correct virtual host." + return 0 +} + +# Function to check if domain mapping already exists in a listener +domain_mapping_exists() { + local listener_name="$1" + local config_file="/var/www/conf/httpd_config.xml" + + # Check if the domain is already mapped in this listener + if grep -A20 "$listener_name" "$config_file" | grep -q "$DOMAIN"; then + return 0 # Domain mapping exists + else + return 1 # Domain mapping doesn't exist + fi +} + +# Ensure XML validation tools are installed +install_xml_tools() { + log "Checking for XML validation tools..." + if ! command -v xmllint > /dev/null; then + log "Installing XML validation tools..." + if grep -q "AlmaLinux" /etc/os-release; then + dnf install -y libxml2 + elif [[ -f /etc/debian_version ]]; then + apt-get update && apt-get install -y libxml2-utils + elif [[ -f /etc/redhat-release ]]; then + yum install -y libxml2 + else + log "WARNING: Cannot install XML validation tools automatically. Manual validation will be skipped." + return 1 + fi + fi + log "XML validation tools are available." + return 0 +} + +# New 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//./_}" # Replace dots with underscores for uniqueness + + log "Checking if virtual host for $domain needs to be created..." + + # Check if virtual host already exists + if grep -q "$vhost_name" "$config_file"; then + log "Virtual host '$vhost_name' already exists, skipping creation." + return 0 + fi + + log "Creating virtual host for $domain..." + local temp_file=$(mktemp) + + # Create the virtual host definition + local vhost_config="\n $vhost_name\n /var/www/webroot/\n \$SERVER_ROOT/conf/vhconf.xml\n 1\n 1\n 1\n 0\n 0\n" + + # Insert the virtual host before the end of virtualHostList tag + awk -v vhost="$vhost_config" ' + /<\/virtualHostList>/ { print " " vhost; } + { print } + ' "$config_file" > "$temp_file" + + # Check if file looks valid + if [ -s "$temp_file" ] && grep -q "" "$temp_file" && grep -q "" "$temp_file"; then + cp "$temp_file" "$config_file" + log "Virtual host for $domain created successfully." + else + log "Error: Generated configuration appears invalid. Virtual host not created." + rm -f "$temp_file" + return 1 + fi + + rm -f "$temp_file" + return 0 +} + +# Add a new function to create a domain-specific HTTPS listener +create_domain_listener() { + local domain="$1" + local key_file="/etc/letsencrypt/live/$domain/privkey.pem" + local cert_file="/etc/letsencrypt/live/$domain/fullchain.pem" + local config_file="/var/www/conf/httpd_config.xml" + local vhost_name="${domain%%.*}" + + log "Creating domain-specific HTTPS listener for $domain..." + + # Check if listener already exists + if grep -q "HTTPS-$domain" "$config_file"; then + log "HTTPS listener for $domain already exists, updating certificate paths..." + sed -i "/HTTPS-$domain<\/name>/,/<\/listener>/s|.*|$key_file|" "$config_file" + sed -i "/HTTPS-$domain<\/name>/,/<\/listener>/s|.*|$cert_file|" "$config_file" + return 0 + fi + + # Create a new listener with domain-specific settings + local temp_file=$(mktemp) + + # Insert the new listener before the end of listenerList + awk -v domain="$domain" -v vhost="$vhost_name" -v key="$key_file" -v cert="$cert_file" ' + /<\/listenerList>/ { + print " " + print " HTTPS-" domain "" + print "
*:443
" + print " 1" + print " " + print " " + print " " vhost "" + print " " domain "" + print " " + print " " + print " " key "" + print " " cert "" + print " 1" + print " 24" + print " ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384" + print " 1" + print " 1" + print " 15" + print "
" + } + { print } + ' "$config_file" > "$temp_file" + + mv "$temp_file" "$config_file" + log "Domain-specific HTTPS listener created for $domain." +} + # Parse input parameters for arg in "$@"; do case $arg in @@ -203,22 +570,73 @@ 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 - yum install -y certbot + # 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 +# 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 + exit 0 + else + log "Certificate expires in $DAYS_LEFT days. Proceeding with renewal." + fi +fi + # Issue SSL certificate CERTBOT_CMD="certbot certonly --webroot -w /var/www/webroot/ROOT -d $DOMAIN --agree-tos --non-interactive" [[ -n "${EMAIL:-}" ]] && CERTBOT_CMD+=" --email $EMAIL" + +# Improved LiteSpeed service handling +restart_litespeed() { + log "Restarting LiteSpeed web server..." + if systemctl is-active --quiet lsws; then + systemctl reload lsws || systemctl restart lsws + log "LiteSpeed successfully restarted." + else + systemctl start lsws + log "LiteSpeed was not running. Started the service." + fi +} + +# 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 - sudo systemctl reload lsws - send_email "$DOMAIN SSL Certificate Issued Successfully" "The SSL certificate for $DOMAIN has been successfully installed." - setup_cron_job + + # 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."