287 lines
8.8 KiB
Bash
287 lines
8.8 KiB
Bash
#!/bin/bash
|
|
# ==============================================================================
|
|
# Script Name: ssl_manager.sh
|
|
# Description: Automates SSL certificate issuance and updates LiteSpeed's httpd_config.xml.
|
|
# Ensures robust backups, validation, and error-free updates.
|
|
# Version: 2.0.1 (Optimized and Error-Free)
|
|
# Author: Anthony Garces (tony@mightybox.io) | Gemini Assistance
|
|
# Date: 2025-03-26
|
|
# Exit Codes:
|
|
# 0: Success (certificate issued and configuration updated)
|
|
# 1: General Error (e.g., failed to issue certificate, XML validation failed)
|
|
# 2: Backup/Restore Error (e.g., failed to create or restore backup)
|
|
# 3: Restart Error (e.g., LiteSpeed service failed to restart)
|
|
# ==============================================================================
|
|
set -euo pipefail
|
|
|
|
# === Configuration ===
|
|
CONF_FILE="/var/www/conf/httpd_config.xml"
|
|
DEFAULT_CONF="/var/www/conf/httpd_config.default.xml"
|
|
SERVER_ROOT="/var/www"
|
|
LOG_DIR="/var/log/mb-ssl"
|
|
CERT_DIR="/etc/letsencrypt/live"
|
|
PUBLIC_IP="" # Placeholder for public IP address
|
|
DOMAINS=() # Placeholder for domains (array)
|
|
EMAIL="" # Placeholder for email address
|
|
VERBOSE=0
|
|
|
|
# - Internal Variables -
|
|
BACKUP_FILE="${LOG_DIR}/httpd_config_backup_$(date +%Y%m%d%H%M%S).xml"
|
|
XML_ERROR_LOG="${LOG_DIR}/xml-edit-error.log"
|
|
SCRIPT_LOG="${LOG_DIR}/ssl_manager.log"
|
|
|
|
# === Cleanup Trap ===
|
|
trap 'log "Script interrupted. Cleaning up temporary files..."; sudo rm -f "$BACKUP_FILE"' EXIT
|
|
|
|
# === Functions ===
|
|
log() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | sudo tee -a "$SCRIPT_LOG"
|
|
}
|
|
|
|
log_verbose() {
|
|
[[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
|
|
}
|
|
|
|
check_command() {
|
|
local cmd="$1"
|
|
local apt_pkg="$2"
|
|
if ! command -v "$cmd" &>/dev/null; then
|
|
log "⚠ Required command '$cmd' not found. Attempting to install package '$apt_pkg'..."
|
|
if sudo yum install -y "$apt_pkg"; then
|
|
log "✔ Successfully installed '$apt_pkg'."
|
|
else
|
|
log "❌ ERROR: Failed to install '$apt_pkg'. Exiting."
|
|
exit 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
validate_domain() {
|
|
local domain="$1"
|
|
if [[ "$domain" =~ ^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$ ]]; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
validate_ip() {
|
|
local ip="$1"
|
|
if [[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
validate_email() {
|
|
local email="$1"
|
|
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
create_default_backup() {
|
|
if [[ ! -f "$DEFAULT_CONF" ]]; then
|
|
log "Creating initial backup of '$CONF_FILE' as '$DEFAULT_CONF'..."
|
|
if sudo cp -a "$CONF_FILE" "$DEFAULT_CONF"; then
|
|
log "✔ Initial backup created: '$DEFAULT_CONF'."
|
|
else
|
|
log "❌ FATAL: Failed to create initial backup '$DEFAULT_CONF'. Exiting."
|
|
exit 2
|
|
fi
|
|
else
|
|
log "Info: Default backup '$DEFAULT_CONF' already exists. Skipping creation."
|
|
fi
|
|
}
|
|
|
|
validate_xml() {
|
|
local file_to_validate="$1"
|
|
log_verbose "Validating XML structure of '$file_to_validate'..."
|
|
if sudo xmllint --noout "$file_to_validate" 2>/dev/null; then
|
|
log_verbose "✔ XML structure of '$file_to_validate' is valid."
|
|
return 0
|
|
else
|
|
log "❌ ERROR: XML structure of '$file_to_validate' is invalid."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
validate_dns() {
|
|
local domain="$1"
|
|
local expected_ip="$2"
|
|
log "Validating DNS resolution for '$domain'..."
|
|
local resolved_ip
|
|
resolved_ip=$(dig +short "$domain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)
|
|
if [[ "$resolved_ip" == "$expected_ip" ]]; then
|
|
log "✔ Domain '$domain' resolves to the expected IP '$expected_ip'."
|
|
return 0
|
|
else
|
|
log "❌ ERROR: Domain '$domain' does NOT resolve to the expected IP ('$expected_ip'). Resolved to '$resolved_ip'."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
validate_http_access() {
|
|
local domain="$1"
|
|
log "Validating HTTP access for '$domain'..."
|
|
local token
|
|
token=$(openssl rand -hex 16)
|
|
echo "$token" > "/var/www/webroot/ROOT/.well-known/acme-challenge/test-token"
|
|
local response
|
|
response=$(curl -s "http://$domain/.well-known/acme-challenge/test-token")
|
|
if [[ "$response" == "$token" ]]; then
|
|
log "✔ HTTP access verified for '$domain'."
|
|
return 0
|
|
else
|
|
log "❌ ERROR: HTTP access check failed for '$domain'. Response: '$response'."
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
issue_certificate() {
|
|
local domain="$1"
|
|
local email="$2"
|
|
log "Issuing SSL certificate for domain '$domain' with email '$email'..."
|
|
if sudo certbot certonly --standalone --preferred-challenges http -d "$domain" --non-interactive --agree-tos --email "$email"; then
|
|
log "✔ SSL certificate issued successfully for '$domain'."
|
|
else
|
|
log "❌ ERROR: Failed to issue SSL certificate for '$domain'."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
update_httpd_config() {
|
|
local domain="$1"
|
|
local ip="$2"
|
|
log "Updating httpd_config.xml for domain '$domain' with IP '$ip'..."
|
|
|
|
# Create a backup of the current configuration
|
|
log "Creating timestamped backup of '$CONF_FILE' as '$BACKUP_FILE'..."
|
|
if sudo cp -a "$CONF_FILE" "$BACKUP_FILE"; then
|
|
log "✔ Backup created: '$BACKUP_FILE'."
|
|
else
|
|
log "❌ ERROR: Failed to create backup of '$CONF_FILE'. Exiting."
|
|
exit 2
|
|
fi
|
|
|
|
# Update the listener configuration using xmlstarlet
|
|
log "Adding listener for domain '$domain' with IP '$ip'..."
|
|
sudo xmlstarlet ed -L \
|
|
-s "//listenerList" -t elem -n "listener" \
|
|
-s "//listener[last()]" -t elem -n "name" -v "$domain" \
|
|
-s "//listener[last()]" -t elem -n "address" -v "$ip:443" \
|
|
-s "//listener[last()]" -t elem -n "secure" -v "1" \
|
|
-s "//listener[last()]" -t elem -n "keyFile" -v "/etc/letsencrypt/live/$domain/privkey.pem" \
|
|
-s "//listener[last()]" -t elem -n "certFile" -v "/etc/letsencrypt/live/$domain/fullchain.pem" \
|
|
"$CONF_FILE"
|
|
|
|
# Validate the updated configuration
|
|
if validate_xml "$CONF_FILE"; then
|
|
log "✔ Updated configuration is valid."
|
|
else
|
|
log "❌ ERROR: Updated configuration is invalid. Restoring backup..."
|
|
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
|
|
log "✔ Backup restored: '$CONF_FILE'."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
restart_litespeed() {
|
|
log "Restarting LiteSpeed server..."
|
|
if sudo systemctl restart lsws; then
|
|
log "✔ LiteSpeed server restarted successfully."
|
|
else
|
|
log "❌ ERROR: Failed to restart LiteSpeed server. Restoring backup..."
|
|
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
|
|
log "✔ Backup restored: '$CONF_FILE'."
|
|
exit 3
|
|
fi
|
|
}
|
|
|
|
# === Main Script Logic ===
|
|
log "Starting SSL Manager V2.0.1..."
|
|
|
|
# Ensure log directory exists
|
|
sudo mkdir -p "$LOG_DIR" || { log "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
|
|
sudo touch "$SCRIPT_LOG" "$XML_ERROR_LOG"
|
|
sudo chown "$(whoami)":"$(id -gn)" "$SCRIPT_LOG" "$XML_ERROR_LOG" 2>/dev/null || log_verbose "Info: Could not change ownership of log files (may require sudo)."
|
|
|
|
# Argument Parsing
|
|
for arg in "$@"; do
|
|
case $arg in
|
|
--public-ip=*)
|
|
PUBLIC_IP="${arg#*=}"
|
|
;;
|
|
--domains=*)
|
|
IFS=',' read -ra DOMAINS <<< "${arg#*=}"
|
|
PRIMARY_DOMAIN="${DOMAINS[0]}"
|
|
;;
|
|
--email=*)
|
|
EMAIL="${arg#*=}"
|
|
;;
|
|
--verbose)
|
|
VERBOSE=1
|
|
log "Verbose mode enabled."
|
|
;;
|
|
*)
|
|
log "Invalid argument: $arg"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Input Validation
|
|
if [[ -z "$PRIMARY_DOMAIN" || -z "$PUBLIC_IP" || -z "$EMAIL" ]]; then
|
|
log "❌ ERROR: Missing required parameters. Provide --domains, --public-ip, and --email."
|
|
exit 1
|
|
fi
|
|
|
|
if ! validate_domain "$PRIMARY_DOMAIN"; then
|
|
log "❌ ERROR: Invalid domain '$PRIMARY_DOMAIN'."
|
|
exit 1
|
|
fi
|
|
|
|
if ! validate_ip "$PUBLIC_IP"; then
|
|
log "❌ ERROR: Invalid IP address '$PUBLIC_IP'."
|
|
exit 1
|
|
fi
|
|
|
|
if ! validate_email "$EMAIL"; then
|
|
log "❌ ERROR: Invalid email address '$EMAIL'."
|
|
exit 1
|
|
fi
|
|
|
|
# Validate DNS and HTTP Access
|
|
if ! validate_dns "$PRIMARY_DOMAIN" "$PUBLIC_IP"; then
|
|
log "❌ ERROR: DNS validation failed. Exiting."
|
|
exit 1
|
|
fi
|
|
|
|
if ! validate_http_access "$PRIMARY_DOMAIN"; then
|
|
log "❌ ERROR: HTTP access validation failed. Exiting."
|
|
exit 1
|
|
fi
|
|
|
|
# Dependency Checks
|
|
log_verbose "Checking dependencies..."
|
|
check_command "xmllint" "libxml2-utils"
|
|
check_command "xmlstarlet" "xmlstarlet"
|
|
check_command "certbot" "certbot"
|
|
|
|
# Create Default Backup
|
|
create_default_backup
|
|
|
|
# Issue SSL Certificate
|
|
issue_certificate "$PRIMARY_DOMAIN" "$EMAIL"
|
|
|
|
# Update httpd_config.xml
|
|
update_httpd_config "$PRIMARY_DOMAIN" "$PUBLIC_IP"
|
|
|
|
# Restart LiteSpeed
|
|
restart_litespeed
|
|
|
|
log "✅ SSL Manager completed successfully. Certificate issued and configuration updated."
|
|
exit 0 |