1025 lines
36 KiB
Bash
1025 lines
36 KiB
Bash
#!/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 "<httpServerConfig>" "$config_file")
|
|
local close_http=$(grep -c "</httpServerConfig>" "$config_file")
|
|
local open_listeners=$(grep -c "<listener>" "$config_file")
|
|
local close_listeners=$(grep -c "</listener>" "$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 <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
|
|
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 "<name>HTTPS-$domain</name>" "$config_file"; then
|
|
log "HTTPS listener for $domain already exists, updating configuration..."
|
|
|
|
# Update certificate paths in existing listener
|
|
sed -i "/<name>HTTPS-$domain<\/name>/,/<\/listener>/ s|<keyFile>.*</keyFile>|<keyFile>$key_file</keyFile>|" "$config_file"
|
|
sed -i "/<name>HTTPS-$domain<\/name>/,/<\/listener>/ s|<certFile>.*</certFile>|<certFile>$cert_file</certFile>|" "$config_file"
|
|
|
|
# Verify updates were applied
|
|
if grep -A5 "<name>HTTPS-$domain</name>" "$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 " <listener>"
|
|
print " <name>HTTPS-" domain "</name>"
|
|
print " <address>*:443</address>"
|
|
print " <secure>1</secure>"
|
|
print " <vhostMapList>"
|
|
print " <vhostMap>"
|
|
print " <vhost>" vhost "</vhost>"
|
|
print " <domain>" domain "</domain>"
|
|
print " </vhostMap>"
|
|
print " </vhostMapList>"
|
|
print " <keyFile>" key "</keyFile>"
|
|
print " <certFile>" cert "</certFile>"
|
|
print " <certChain>1</certChain>"
|
|
print " <sslProtocol>24</sslProtocol>"
|
|
print " <ciphers>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</ciphers>"
|
|
print " <sslSessionCache>1</sslSessionCache>"
|
|
print " <sslSessionTickets>1</sslSessionTickets>"
|
|
print " <enableSpdy>15</enableSpdy>"
|
|
print " </listener>"
|
|
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 "<httpServerConfig>" "$temp_file" || ! grep -q "</httpServerConfig>" "$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 "<name>$vhost_name</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 " <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."
|
|
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
|
|
awk -v domain="$domain" '
|
|
BEGIN { skip = 0; }
|
|
/<name>HTTPS-'"$domain"'<\/name>/,/<\/listener>/ {
|
|
if ($0 ~ /<name>HTTPS-'"$domain"'<\/name>/) {
|
|
skip = 1;
|
|
print "<!-- Listener for " domain " removed by ssl_manager.sh -->";
|
|
}
|
|
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//[.]/_}"
|
|
|
|
awk -v vhost="$vhost_name" '
|
|
BEGIN { skip = 0; }
|
|
/<name>'"$vhost"'<\/name>/,/<\/virtualHost>/ {
|
|
if ($0 ~ /<name>'"$vhost"'<\/name>/) {
|
|
skip = 1;
|
|
print "<!-- VirtualHost for " vhost " removed by ssl_manager.sh -->";
|
|
}
|
|
if ($0 ~ /<\/virtualHost>/ && skip == 1) {
|
|
skip = 0;
|
|
next;
|
|
}
|
|
if (skip) next;
|
|
}
|
|
{ if (!skip) print; }
|
|
' "$temp_file" > "${temp_file}.new"
|
|
|
|
# 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 = ""; }
|
|
/<vhostMap>/ {
|
|
in_vhostmap = 1;
|
|
vhostmap_buffer = $0 "\n";
|
|
next;
|
|
}
|
|
in_vhostmap == 1 {
|
|
vhostmap_buffer = vhostmap_buffer $0 "\n";
|
|
if ($0 ~ /<domain>'"$domain"'<\/domain>/) {
|
|
skip_vhostmap = 1;
|
|
}
|
|
if ($0 ~ /<\/vhostMap>/) {
|
|
if (skip_vhostmap == 0) {
|
|
printf "%s", vhostmap_buffer;
|
|
} else {
|
|
print "<!-- Domain mapping for " domain " removed -->";
|
|
}
|
|
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 "<httpServerConfig>" "${temp_file}.final" || ! grep -q "</httpServerConfig>" "${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..."
|
|
find /var/log/lsws/ -name "*$domain*" -type f -delete
|
|
|
|
# 9. Clean related cache files
|
|
log "Cleaning related cache files..."
|
|
find /var/www/webroot/ROOT/.well-known/acme-challenge/ -type f -delete 2>/dev/null
|
|
|
|
# 10. Restart LiteSpeed to apply configuration changes
|
|
if restart_litespeed; then
|
|
log "LiteSpeed restarted successfully after certificate removal."
|
|
else
|
|
log "ERROR: Failed to restart LiteSpeed after certificate removal."
|
|
return 1
|
|
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
|