2025-01-29 13:03:24 +00:00
#!/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
}
2025-03-20 18:07:43 +00:00
# Function to update LiteSpeed configuration - Updated with virtual host fix
2025-01-29 13:03:24 +00:00
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 "
2025-03-20 18:07:43 +00:00
local timestamp = $( date +%Y%m%d%H%M%S)
local backup_file = " ${ config_file } .backup. ${ timestamp } "
2025-01-29 13:03:24 +00:00
log "Updating LiteSpeed configuration..."
2025-03-20 18:07:43 +00:00
# Ensure we have a backup with timestamp
cp " $config_file " " $backup_file "
log " Created backup of LiteSpeed configuration at $backup_file "
2025-03-20 18:22:22 +00:00
# Clean up any redundant listeners for this domain
2025-03-20 18:07:43 +00:00
cleanup_redundant_listeners " $config_file " " $DOMAIN "
2025-03-20 18:22:22 +00:00
# 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
2025-01-29 13:03:24 +00:00
fi
2025-03-20 18:07:43 +00:00
2025-03-20 18:22:22 +00:00
# Create domain-specific listener
if ! create_domain_listener " $DOMAIN " ; then
log " ERROR: Failed to create listener for $DOMAIN . Aborting configuration update. "
return 1
2025-03-20 18:07:43 +00:00
fi
2025-03-20 18:22:22 +00:00
# Remove domain from shared listeners - safer to avoid certificate mismatch errors
remove_domain_from_shared_listeners
2025-03-20 18:07:43 +00:00
# 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
2025-03-20 18:22:22 +00:00
log "LiteSpeed configuration updated successfully with dedicated domain configuration."
2025-03-20 18:07:43 +00:00
return 0
2025-01-29 13:03:24 +00:00
}
# 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
}
2025-03-20 18:07:43 +00:00
# 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
}
2025-03-20 18:22:22 +00:00
# Function to create or update a domain-specific HTTPS listener
2025-03-20 18:07:43 +00:00
create_domain_listener( ) {
local domain = " $1 "
2025-03-20 18:22:22 +00:00
local config_file = "/var/www/conf/httpd_config.xml"
local vhost_name = " ${ domain //[.]/_ } "
2025-03-20 18:07:43 +00:00
local key_file = " /etc/letsencrypt/live/ $domain /privkey.pem "
local cert_file = " /etc/letsencrypt/live/ $domain /fullchain.pem "
2025-03-20 18:22:22 +00:00
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 ... "
2025-03-20 18:07:43 +00:00
2025-03-20 18:22:22 +00:00
# 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
2025-03-20 18:07:43 +00:00
# Check if listener already exists
if grep -q " <name>HTTPS- $domain </name> " " $config_file " ; then
2025-03-20 18:22:22 +00:00
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
2025-03-20 18:07:43 +00:00
return 0
fi
2025-03-20 18:22:22 +00:00
log " Creating new HTTPS listener for $domain ... "
# Create a temporary file for XML editing
2025-03-20 18:07:43 +00:00
local temp_file = $( mktemp)
2025-03-20 18:22:22 +00:00
if [ ! -f " $temp_file " ] ; then
log "ERROR: Failed to create temporary file for configuration update."
return 1
fi
2025-03-20 18:07:43 +00:00
2025-03-20 18:22:22 +00:00
# Insert new listener into configuration before listenerList end tag
2025-03-20 18:07:43 +00:00
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>"
2025-03-20 18:22:22 +00:00
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
2025-03-20 18:07:43 +00:00
}
{ print }
' " $config_file " > " $temp_file "
2025-03-20 18:22:22 +00:00
# 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
2025-03-20 18:07:43 +00:00
}
2025-03-21 17:29:22 +00:00
# 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
2025-03-21 17:51:15 +00:00
# Remove the HTTPS-domain listener section
local domain_pattern = " HTTPS- ${ domain } "
awk -v domain = " $domain " -v pattern = " $domain_pattern " '
2025-03-21 17:29:22 +00:00
BEGIN { skip = 0; }
2025-03-21 17:51:15 +00:00
$0 ~ pattern,/<\/ listener>/ {
if ( $0 ~ pattern) {
2025-03-21 17:29:22 +00:00
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 //[.]/_ } "
2025-03-21 17:51:15 +00:00
# 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 " '
2025-03-21 17:37:21 +00:00
BEGIN { skip = 0; }
2025-03-21 17:51:15 +00:00
$0 ~ pattern,/<\/ virtualHost>/ {
if ( $0 ~ pattern) {
2025-03-21 17:37:21 +00:00
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 "
2025-03-21 17:51:15 +00:00
else
log " No virtual host found for ${ vhost_name } , skipping this step. " ;
cp " $temp_file " " ${ temp_file } .new "
2025-03-21 17:37:21 +00:00
fi
2025-03-21 17:29:22 +00:00
# 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 ... "
2025-03-21 17:51:15 +00:00
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
2025-03-21 17:29:22 +00:00
# 9. Clean related cache files
log "Cleaning related cache files..."
2025-03-21 17:51:15 +00:00
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
2025-03-21 17:29:22 +00:00
2025-03-21 17:51:15 +00:00
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
2025-03-21 17:29:22 +00:00
else
2025-03-21 17:51:15 +00:00
log "No configuration changes detected. Skipping LiteSpeed restart."
2025-03-21 17:29:22 +00:00
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
}
2025-01-29 13:03:24 +00:00
# Parse input parameters
for arg in " $@ " ; do
case $arg in
--public-ip= *)
PUBLIC_IP = " ${ arg #*= } "
; ;
--domain= *)
DOMAIN = " ${ arg #*= } "
; ;
--email= *)
EMAIL = " ${ arg #*= } "
; ;
2025-03-21 17:19:28 +00:00
--remove-cert= *)
REMOVE_CERT = " ${ arg #*= } "
; ;
--confirm= *)
CONFIRM = " ${ arg #*= } "
; ;
2025-01-29 13:03:24 +00:00
*)
echo " Invalid argument: $arg "
exit 1
; ;
esac
done
2025-03-21 17:19:28 +00:00
# Check for certificate removal request
if [ [ -n " ${ REMOVE_CERT :- } " ] ] ; then
if [ [ " ${ REMOVE_CERT } " = = "yes" ] ] ; then
remove_ssl_certificate " ${ DOMAIN } " " ${ CONFIRM :- no } "
exit $?
fi
fi
2025-01-29 13:03:24 +00:00
# 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
2025-03-20 18:07:43 +00:00
# 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
2025-01-29 13:03:24 +00:00
else
echo "Unsupported OS. Install Certbot manually."
exit 1
fi
fi
2025-03-20 18:07:43 +00:00
# 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
2025-01-29 13:03:24 +00:00
# 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 "
2025-03-20 18:07:43 +00:00
# After Certbot installation and before existing certificate check
install_xml_tools
# Replace the simple reload with the improved function
2025-01-29 13:03:24 +00:00
if $CERTBOT_CMD ; then
log " SSL certificate issued successfully for $DOMAIN . "
2025-03-20 18:07:43 +00:00
# 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
2025-01-29 13:03:24 +00:00
else
log "Certbot failed."
send_email "SSL Certificate Installation Failed" " An error occurred while installing the SSL certificate for $DOMAIN . "
exit 1
fi