#!/bin/bash set -euo pipefail # Log file setup 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 # Function to log messages log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" } # 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 } # 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() { 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; 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 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 } # 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 if [ ! -f "$backup_file" ]; then cp "$config_file" "$backup_file" log "Created backup of LiteSpeed configuration at $backup_file" fi # Check if listener already exists if grep -q "HTTPS-$domain" "$config_file"; then log "HTTPS listener for $domain already exists, updating configuration..." # Update certificate paths in existing listener sed -i "/HTTPS-$domain<\/name>/,/<\/listener>/ s|.*|$key_file|" "$config_file" sed -i "/HTTPS-$domain<\/name>/,/<\/listener>/ s|.*|$cert_file|" "$config_file" # Verify updates were applied if grep -A5 "HTTPS-$domain" "$config_file" | grep -q "$key_file"; then log "Certificate paths updated successfully for $domain listener." else log "ERROR: Failed to update certificate paths for $domain listener." return 1 fi return 0 fi log "Creating new HTTPS listener for $domain..." # Create a temporary file for XML editing local temp_file=$(mktemp) if [ ! -f "$temp_file" ]; then log "ERROR: Failed to create temporary file for configuration update." return 1 fi # Insert new listener into configuration before listenerList end tag 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 $0 next } { print } ' "$config_file" > "$temp_file" # Validate the temporary file if [ ! -s "$temp_file" ]; then log "ERROR: Generated configuration is empty. Keeping original configuration." rm -f "$temp_file" return 1 fi # Check for basic XML validity if ! grep -q "" "$temp_file" || ! grep -q "" "$temp_file"; then log "ERROR: Generated configuration appears invalid. 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 file. Keeping original configuration." rm -f "$temp_file" return 1 fi # Clean up temp file rm -f "$temp_file" 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 "$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) if [ ! -f "$temp_file" ]; then log "ERROR: Failed to create temporary file for virtual host creation." return 1 fi # Insert new virtual host before virtualHostList end tag awk -v vhost="$vhost_name" ' /<\/virtualHostList>/ { print " " print " " vhost "" print " /var/www/webroot/" print " $SERVER_ROOT/conf/vhconf.xml" print " 1" print " 1" print " 1" print " 0" print " 0" print " " 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" ' /HTTPS<\/name>/,/<\/listener>/ { if ($0 ~ //) { 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" ' /HTTPS-ipv6<\/name>/,/<\/listener>/ { if ($0 ~ //) { 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 } # Restart LiteSpeed with extra verification restart_litespeed() { log "Restarting LiteSpeed web server..." # Verify configuration before restart if command -v /usr/local/lsws/bin/lshttpd > /dev/null; then log "Verifying LiteSpeed configuration before restart..." /usr/local/lsws/bin/lshttpd -t if [ $? -ne 0 ]; then log "ERROR: LiteSpeed configuration test failed. Not restarting server." return 1 fi log "LiteSpeed configuration verified successfully." fi # Now restart the service if systemctl is-active --quiet lsws; then systemctl restart lsws if [ $? -ne 0 ]; then log "ERROR: Failed to restart LiteSpeed. Please check logs." return 1 fi # Verify LiteSpeed is running after restart sleep 2 if ! systemctl is-active --quiet lsws; then log "ERROR: LiteSpeed failed to start after restart. Please check logs." return 1 fi log "LiteSpeed successfully restarted." else systemctl start lsws if [ $? -ne 0 ]; then log "ERROR: Failed to start LiteSpeed. Please check logs." return 1 fi log "LiteSpeed was not running. Started the service." fi return 0 } # Function to remove SSL certificate and its configuration remove_ssl_certificate() { local domain="$1" local confirm="${2:-no}" if [[ -z "$domain" ]]; then log "Error: Domain parameter is required for certificate removal." return 1 fi # Check if certificate exists if [[ ! -d "/etc/letsencrypt/live/$domain" && ! -d "/etc/letsencrypt/archive/$domain" ]]; then log "Certificate for $domain not found. Nothing to remove." return 1 fi # Confirm removal if not forced if [[ "$confirm" != "yes" ]]; then log "WARNING: This will remove the SSL certificate for $domain and update LiteSpeed configuration." log "Please run again with --confirm=yes to proceed with removal." return 1 fi log "Starting removal of SSL certificate for $domain..." # 1. Backup LiteSpeed configuration before making changes local config_file="/var/www/conf/httpd_config.xml" local vhost_config="/var/www/conf/vhconf.xml" local timestamp=$(date +%Y%m%d%H%M%S) local backup_file="${config_file}.removal.${timestamp}" local vhost_backup="${vhost_config}.removal.${timestamp}" cp "$config_file" "$backup_file" log "Created backup of LiteSpeed configuration at $backup_file" if [ -f "$vhost_config" ]; then cp "$vhost_config" "$vhost_backup" log "Created backup of virtual host configuration at $vhost_backup" fi # 2. Remove domain-specific listener from LiteSpeed configuration log "Removing domain-specific listener from LiteSpeed configuration..." local temp_file=$(mktemp) if [ ! -f "$temp_file" ]; then log "ERROR: Failed to create temporary file for configuration update." return 1 fi # Remove the HTTPS-domain listener section local domain_pattern="HTTPS-${domain}" awk -v domain="$domain" -v pattern="$domain_pattern" ' BEGIN { skip = 0; } $0 ~ pattern,/<\/listener>/ { if ($0 ~ pattern) { skip = 1; print ""; } if ($0 ~ /<\/listener>/ && skip == 1) { skip = 0; next; } if (skip) next; } { if (!skip) print; } ' "$config_file" > "$temp_file" # 3. Remove from domain-specific virtual host if it exists log "Removing domain-specific virtual host if it exists..." local vhost_name="${domain//[.]/_}" # Check if virtualhost exists (safer approach) if grep -q "$vhost_name" "$config_file"; then # Process only if virtual host might exist local vhost_pattern="$vhost_name" awk -v vhost="$vhost_name" -v pattern="$vhost_pattern" ' BEGIN { skip = 0; } $0 ~ pattern,/<\/virtualHost>/ { if ($0 ~ pattern) { skip = 1; print ""; } if ($0 ~ /<\/virtualHost>/ && skip == 1) { skip = 0; next; } if (skip) next; } { if (!skip) print; } ' "$temp_file" > "${temp_file}.new" else log "No virtual host found for ${vhost_name}, skipping this step."; cp "$temp_file" "${temp_file}.new" fi # 4. Remove any domain mappings from shared listeners log "Removing domain mappings from shared listeners..." awk -v domain="$domain" ' BEGIN { in_vhostmap = 0; skip_vhostmap = 0; vhostmap_buffer = ""; } // { in_vhostmap = 1; vhostmap_buffer = $0 "\n"; next; } in_vhostmap == 1 { vhostmap_buffer = vhostmap_buffer $0 "\n"; if ($0 ~ /'"$domain"'<\/domain>/) { skip_vhostmap = 1; } if ($0 ~ /<\/vhostMap>/) { if (skip_vhostmap == 0) { printf "%s", vhostmap_buffer; } else { print ""; } in_vhostmap = 0; skip_vhostmap = 0; vhostmap_buffer = ""; } next; } { print; } ' "${temp_file}.new" > "${temp_file}.final" # Verify the processed file is valid if [ ! -s "${temp_file}.final" ]; then log "ERROR: Generated configuration is empty. Keeping original configuration." rm -f "$temp_file" "${temp_file}.new" "${temp_file}.final" return 1 fi # Check for basic XML validity (main structure tags) if ! grep -q "" "${temp_file}.final" || ! grep -q "" "${temp_file}.final"; then log "ERROR: Generated configuration appears invalid. Keeping original configuration." rm -f "$temp_file" "${temp_file}.new" "${temp_file}.final" return 1 fi # Apply changes cp "${temp_file}.final" "$config_file" rm -f "$temp_file" "${temp_file}.new" "${temp_file}.final" # 5. Clean up any references in vhconf.xml files log "Cleaning up references in vhost configuration files..." find /var/www/conf -name "vhconf.xml" -type f -exec grep -l "$domain" {} \; | while read vhconf_file; do log "Cleaning references in $vhconf_file..." sed -i "/$domain/d" "$vhconf_file" done # 6. Use certbot to revoke and delete the certificate log "Revoking and removing certificate using Certbot..." if certbot revoke --cert-name "$domain" --delete-after-revoke --non-interactive; then log "Certificate for $domain successfully revoked and removed." else # If certbot revoke fails, try direct removal log "Certbot revoke failed. Attempting direct removal of certificate files..." rm -rf "/etc/letsencrypt/live/$domain" "/etc/letsencrypt/archive/$domain" "/etc/letsencrypt/renewal/$domain.conf" # Remove any symlinks that might point to the domain find /etc/letsencrypt -type l -exec ls -l {} \; | grep "$domain" | cut -d " " -f 9 | xargs -r rm log "Certificate files for $domain removed directly." fi # 7. Clean up Apache configuration if exists (some servers might have Apache installed) if [ -d "/etc/apache2" ]; then log "Checking for Apache configuration references..." find /etc/apache2 -name "*.conf" -type f -exec grep -l "$domain" {} \; | while read apache_conf; do log "Cleaning references in $apache_conf..." sed -i "/$domain/d" "$apache_conf" done elif [ -d "/etc/httpd" ]; then log "Checking for Apache configuration references..." find /etc/httpd -name "*.conf" -type f -exec grep -l "$domain" {} \; | while read apache_conf; do log "Cleaning references in $apache_conf..." sed -i "/$domain/d" "$apache_conf" done fi # 8. Clean up LiteSpeed logs for this domain log "Cleaning up log files for $domain..." if [ -d "/var/log/lsws/" ]; then find /var/log/lsws/ -name "*$domain*" -type f -delete 2>/dev/null || true else log "LiteSpeed log directory '/var/log/lsws/' not found, skipping log cleanup." fi # 9. Clean related cache files log "Cleaning related cache files..." if [ -d "/var/www/webroot/ROOT/.well-known/acme-challenge/" ]; then find /var/www/webroot/ROOT/.well-known/acme-challenge/ -type f -delete 2>/dev/null || true else log "ACME challenge directory not found, skipping cache cleanup." fi # 10. Restart LiteSpeed only if it's running and configuration was changed local config_changed=false if grep -q "removed by ssl_manager.sh" "$config_file"; then config_changed=true fi if $config_changed; then log "Configuration changes detected. Restarting LiteSpeed to apply changes..." if restart_litespeed; then log "LiteSpeed restarted successfully after certificate removal." else log "WARNING: Failed to restart LiteSpeed after certificate removal. Manual restart may be required." # Don't return error, continue with the successful certificate removal fi else log "No configuration changes detected. Skipping LiteSpeed restart." fi # 11. Send email notification if configured send_email "$domain SSL Certificate Removed" "The SSL certificate for $domain has been successfully removed from the server and all related configuration has been cleaned up." log "SSL certificate removal completed successfully for $domain." return 0 } # Parse input parameters for arg in "$@"; do case $arg in --public-ip=*) PUBLIC_IP="${arg#*=}" ;; --domain=*) DOMAIN="${arg#*=}" ;; --email=*) EMAIL="${arg#*=}" ;; --remove-cert=*) REMOVE_CERT="${arg#*=}" ;; --confirm=*) CONFIRM="${arg#*=}" ;; *) echo "Invalid argument: $arg" exit 1 ;; esac done # Check for certificate removal request if [[ -n "${REMOVE_CERT:-}" ]]; then if [[ "${REMOVE_CERT}" == "yes" ]]; then remove_ssl_certificate "${DOMAIN}" "${CONFIRM:-no}" exit $? fi fi # Input validation log "Validating inputs..." if [[ -z "${PUBLIC_IP:-}" || -z "${DOMAIN:-}" ]]; then echo "Error: --public-ip and --domain are mandatory." exit 1 fi validate_ip "$PUBLIC_IP" || { echo "Invalid public IP: $PUBLIC_IP"; exit 1; } validate_domain "$DOMAIN" || { echo "Invalid domain: $DOMAIN"; exit 1; } if [[ -n "${EMAIL:-}" ]]; then validate_email "$EMAIL" || { echo "Invalid email: $EMAIL"; exit 1; } fi # 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." 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" # 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." exit 1 fi