Optimize idempotent in editing httpd, also optimized listener
parent
a0feb8d70d
commit
16768aa322
85
mbadmin.jps
85
mbadmin.jps
|
@ -34,6 +34,8 @@ onInstall:
|
||||||
- cd /home/litespeed/mbmanager/ssl-manager
|
- cd /home/litespeed/mbmanager/ssl-manager
|
||||||
- curl -OL https://deploy-proxy.mightybox.io/tony/mb-admin/raw/branch/main/scripts/ssl-manager/ssl_manager.sh
|
- curl -OL https://deploy-proxy.mightybox.io/tony/mb-admin/raw/branch/main/scripts/ssl-manager/ssl_manager.sh
|
||||||
- curl -OL https://deploy-proxy.mightybox.io/tony/mb-admin/raw/branch/main/scripts/ssl-manager/ipchecker.sh
|
- curl -OL https://deploy-proxy.mightybox.io/tony/mb-admin/raw/branch/main/scripts/ssl-manager/ipchecker.sh
|
||||||
|
- curl -OL https://deploy-proxy.mightybox.io/tony/mb-admin/raw/branch/main/scripts/ssl-manager/ssl_remover.sh
|
||||||
|
- curl -OL https://deploy-proxy.mightybox.io/tony/mb-admin/raw/branch/main/scripts/ssl-manager/xmlchecker.sh
|
||||||
- chmod +x /home/litespeed/mbmanager/ssl-manager/*.sh
|
- chmod +x /home/litespeed/mbmanager/ssl-manager/*.sh
|
||||||
# Install Certbot for AlmaLinux
|
# Install Certbot for AlmaLinux
|
||||||
- dnf install -y certbot
|
- dnf install -y certbot
|
||||||
|
@ -143,12 +145,6 @@ menu:
|
||||||
action: remove_ssl_cert
|
action: remove_ssl_cert
|
||||||
settings: sslRemoveConfig
|
settings: sslRemoveConfig
|
||||||
successText: "${response.out}"
|
successText: "${response.out}"
|
||||||
- confirmText: Are you sure you want to clean up certificate references?
|
|
||||||
loadingText: Cleaning certificate references...
|
|
||||||
caption: Clean Certificate References
|
|
||||||
action: clean_cert_references
|
|
||||||
settings: cleanCertConfig
|
|
||||||
successText: "Certificate references cleaned successfully for ${settings.domain}"
|
|
||||||
- confirmText: Are you sure you want to diagnose LiteSpeed configuration?
|
- confirmText: Are you sure you want to diagnose LiteSpeed configuration?
|
||||||
loadingText: Analyzing configuration...
|
loadingText: Analyzing configuration...
|
||||||
caption: Diagnose LiteSpeed Config
|
caption: Diagnose LiteSpeed Config
|
||||||
|
@ -589,10 +585,10 @@ actions:
|
||||||
- cmd[cp]:
|
- cmd[cp]:
|
||||||
user: root
|
user: root
|
||||||
commands:
|
commands:
|
||||||
- bash /home/litespeed/mbmanager/ssl-manager/ssl_manager.sh --public-ip="${settings.public_ip}" --domain="${settings.domain}" --email="${settings.email}"
|
- bash /home/litespeed/mbmanager/ssl-manager/ssl_manager.sh --public-ip="${settings.public_ip}" --domain="${settings.domain}" --email="${settings.email}" --verbose
|
||||||
- return:
|
- return:
|
||||||
type: info
|
type: info
|
||||||
message: "SSL certificate for '${settings.domain}' has been issued successfully."
|
message: "SSL certificate issuance process completed."
|
||||||
check_domain_ip:
|
check_domain_ip:
|
||||||
- cmd[cp]:
|
- cmd[cp]:
|
||||||
user: root
|
user: root
|
||||||
|
@ -605,82 +601,15 @@ actions:
|
||||||
- cmd[cp]:
|
- cmd[cp]:
|
||||||
user: root
|
user: root
|
||||||
commands:
|
commands:
|
||||||
- bash /home/litespeed/mbmanager/ssl-manager/ssl_remover.sh --domains="${settings.domains}" ${EMAIL:+--email="${EMAIL}"}
|
- bash /home/litespeed/mbmanager/ssl-manager/ssl_remover.sh --domains="${settings.domains}" --verbose ${EMAIL:+--email="${EMAIL}"}
|
||||||
- return:
|
- return:
|
||||||
type: info
|
type: info
|
||||||
message: "${response.out}"
|
message: "SSL certificate removal process completed."
|
||||||
diagnose_litespeed_config:
|
diagnose_litespeed_config:
|
||||||
- cmd[cp]:
|
- cmd[cp]:
|
||||||
user: root
|
user: root
|
||||||
commands:
|
commands:
|
||||||
- |
|
- bash /home/litespeed/mbmanager/ssl-manager/xmlchecker.sh --verbose
|
||||||
CONF_FILE="/var/www/conf/httpd_config.xml"
|
|
||||||
echo "Analyzing LiteSpeed configuration tags..."
|
|
||||||
echo "-----------------------------------"
|
|
||||||
num_n=$(grep -c '<n>' "${CONF_FILE}")
|
|
||||||
num_n_close=$(grep -c '</n>' "${CONF_FILE}")
|
|
||||||
num_name=$(grep -c '<name>' "${CONF_FILE}")
|
|
||||||
num_name_close=$(grep -c '</name>' "${CONF_FILE}")
|
|
||||||
echo "Number of <n> tags: $num_n"
|
|
||||||
echo "Number of </n> tags: $num_n_close"
|
|
||||||
echo "Number of <name> tags: $num_name"
|
|
||||||
echo "Number of </name> tags: $num_name_close"
|
|
||||||
echo "-----------------------------------"
|
|
||||||
echo "First 5 instances of <n> tags:"
|
|
||||||
grep -n '<n>' "${CONF_FILE}" | head -5
|
|
||||||
echo "-----------------------------------"
|
|
||||||
echo "Testing sed command effectiveness:"
|
|
||||||
cp "${CONF_FILE}" /tmp/test_config.xml
|
|
||||||
sed -i 's#<n>#<name>#g' /tmp/test_config.xml
|
|
||||||
sed -i 's#</n>#</name>#g' /tmp/test_config.xml
|
|
||||||
new_num_n=$(grep -c '<n>' /tmp/test_config.xml)
|
|
||||||
new_num_n_close=$(grep -c '</n>' /tmp/test_config.xml)
|
|
||||||
echo "After sed, remaining <n> tags: $new_num_n"
|
|
||||||
echo "After sed, remaining </n> tags: $new_num_n_close"
|
|
||||||
echo "-----------------------------------"
|
|
||||||
- return:
|
|
||||||
type: info
|
|
||||||
message: "${response.out}"
|
|
||||||
clean_cert_references:
|
|
||||||
- cmd[cp]:
|
|
||||||
user: root
|
|
||||||
commands:
|
|
||||||
- |
|
|
||||||
DOMAIN="${settings.domain}"
|
|
||||||
CONF_FILE="/var/www/conf/httpd_config.xml"
|
|
||||||
BACKUP_FILE="${CONF_FILE}.bak.$(date +%Y%m%d%H%M%S)"
|
|
||||||
|
|
||||||
# Create backup
|
|
||||||
cp "${CONF_FILE}" "${BACKUP_FILE}"
|
|
||||||
echo "Created backup at ${BACKUP_FILE}"
|
|
||||||
|
|
||||||
# Clean up process
|
|
||||||
TEMP_FILE=$(mktemp)
|
|
||||||
awk -v domain="${DOMAIN}" '
|
|
||||||
BEGIN { in_listener = 0; is_shared = 0; }
|
|
||||||
/<listener>/ { in_listener = 1; print; next; }
|
|
||||||
in_listener && (/<name>HTTPS<\/name>/ || /<name>HTTPS-ipv6<\/name>/) {
|
|
||||||
is_shared = 1; print; next;
|
|
||||||
}
|
|
||||||
in_listener && is_shared && /<keyFile>.*live\/'"${DOMAIN}"'\/.*<\/keyFile>/ {
|
|
||||||
print " <keyFile>/var/www/conf/default.key</keyFile>"; next;
|
|
||||||
}
|
|
||||||
in_listener && is_shared && /<certFile>.*live\/'"${DOMAIN}"'\/.*<\/certFile>/ {
|
|
||||||
print " <certFile>/var/www/conf/default.crt</certFile>"; next;
|
|
||||||
}
|
|
||||||
/<\/listener>/ { in_listener = 0; is_shared = 0; print; next; }
|
|
||||||
{ print; }
|
|
||||||
' "${CONF_FILE}" > "${TEMP_FILE}"
|
|
||||||
|
|
||||||
if grep -q "<httpServerConfig>" "${TEMP_FILE}"; then
|
|
||||||
cat "${TEMP_FILE}" > "${CONF_FILE}"
|
|
||||||
rm -f "${TEMP_FILE}"
|
|
||||||
systemctl restart lsws
|
|
||||||
else
|
|
||||||
echo "ERROR: Invalid config generated"
|
|
||||||
rm -f "${TEMP_FILE}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- return:
|
- return:
|
||||||
type: info
|
type: info
|
||||||
message: "${response.out}"
|
message: "${response.out}"
|
||||||
|
|
|
@ -3,285 +3,465 @@
|
||||||
# Script Name: ssl_manager.sh
|
# Script Name: ssl_manager.sh
|
||||||
# Description: Automates SSL certificate issuance and updates LiteSpeed's httpd_config.xml.
|
# Description: Automates SSL certificate issuance and updates LiteSpeed's httpd_config.xml.
|
||||||
# Ensures robust backups, validation, and error-free updates.
|
# Ensures robust backups, validation, and error-free updates.
|
||||||
# Version: 2.0.1 (Optimized and Error-Free)
|
# Version: 2.0.4 (Idempotent, Clean Exit Messages)
|
||||||
# Author: Anthony Garces (tony@mightybox.io) | Gemini Assistance
|
# Author: Anthony Garces (tony@mightybox.io) | Gemini Assistance
|
||||||
# Date: 2025-03-26
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
# === Configuration ===
|
|
||||||
CONF_FILE="/var/www/conf/httpd_config.xml"
|
CONF_FILE="/var/www/conf/httpd_config.xml"
|
||||||
DEFAULT_CONF="/var/www/conf/httpd_config.default.xml"
|
DEFAULT_CONF="/var/www/conf/httpd_config.default.xml"
|
||||||
SERVER_ROOT="/var/www"
|
SERVER_ROOT="/var/www"
|
||||||
LOG_DIR="/var/log/mb-ssl"
|
LOG_DIR="/var/log/mb-ssl"
|
||||||
CERT_DIR="/etc/letsencrypt/live"
|
CERT_DIR="/etc/letsencrypt/live"
|
||||||
PUBLIC_IP="" # Placeholder for public IP address
|
PUBLIC_IP=""
|
||||||
DOMAINS=() # Placeholder for domains (array)
|
DOMAINS=()
|
||||||
EMAIL="" # Placeholder for email address
|
EMAIL=""
|
||||||
VERBOSE=0
|
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"
|
SCRIPT_LOG="${LOG_DIR}/ssl_manager.log"
|
||||||
|
ERROR_LOG="${LOG_DIR}/ssl_manager-error.log"
|
||||||
|
DEBUG_LOG="${LOG_DIR}/ssl_manager-debug.log"
|
||||||
|
BACKUP_FILE="${LOG_DIR}/httpd_config_backup_$(date +%Y%m%d%H%M%S).xml"
|
||||||
|
SCRIPT_EXIT_STATUS=0
|
||||||
|
|
||||||
# === Cleanup Trap ===
|
setup_logging() {
|
||||||
trap 'log "Script interrupted. Cleaning up temporary files..."; sudo rm -f "$BACKUP_FILE"' EXIT
|
# Create log directory if it doesn't exist
|
||||||
|
sudo mkdir -p "$LOG_DIR" || { echo "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
sudo chown -R "$(whoami)":"$(id -gn)" "$LOG_DIR"
|
||||||
|
sudo chmod 755 "$LOG_DIR"
|
||||||
|
|
||||||
|
# Create log files with proper permissions
|
||||||
|
touch "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"
|
||||||
|
chmod 644 "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"
|
||||||
|
|
||||||
|
# Add log rotation if log files are too large (>10MB)
|
||||||
|
for log_file in "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"; do
|
||||||
|
if [ -f "$log_file" ] && [ "$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file")" -gt 10485760 ]; then
|
||||||
|
mv "$log_file" "${log_file}.$(date +%Y%m%d)"
|
||||||
|
touch "$log_file"
|
||||||
|
chmod 644 "$log_file"
|
||||||
|
gzip "${log_file}.$(date +%Y%m%d)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# === Functions ===
|
|
||||||
log() {
|
log() {
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | sudo tee -a "$SCRIPT_LOG"
|
local level="INFO"
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Log to main log file
|
||||||
|
echo "[$timestamp] [$level] $message" | sudo tee -a "$SCRIPT_LOG"
|
||||||
|
|
||||||
|
# Log errors to error log file
|
||||||
|
if [[ "$message" == *"ERROR"* ]] || [[ "$message" == *"❌"* ]]; then
|
||||||
|
echo "[$timestamp] [$level] $message" | sudo tee -a "$ERROR_LOG"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
log_verbose() {
|
log_verbose() {
|
||||||
[[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
|
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [DEBUG] $1" | sudo tee -a "$DEBUG_LOG"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [ERROR] $message" | sudo tee -a "$ERROR_LOG" "$SCRIPT_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [SUCCESS] $message" | sudo tee -a "$SCRIPT_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
on_exit() {
|
||||||
|
if [[ $SCRIPT_EXIT_STATUS -ne 0 ]]; then
|
||||||
|
log_error "Script ended with error. Exit Code: $SCRIPT_EXIT_STATUS"
|
||||||
|
log_error "Backup file retained: $BACKUP_FILE"
|
||||||
|
else
|
||||||
|
sudo rm -f "$BACKUP_FILE"
|
||||||
|
log_success "Script completed successfully. Backup cleaned."
|
||||||
|
fi
|
||||||
|
exit $SCRIPT_EXIT_STATUS
|
||||||
|
}
|
||||||
|
trap 'on_exit' EXIT
|
||||||
|
|
||||||
check_command() {
|
check_command() {
|
||||||
local cmd="$1"
|
local cmd="$1"
|
||||||
local apt_pkg="$2"
|
local apt_pkg="$2"
|
||||||
if ! command -v "$cmd" &>/dev/null; then
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
log "⚠ Required command '$cmd' not found. Attempting to install package '$apt_pkg'..."
|
log "Required command '$cmd' not found. Attempting to install package '$apt_pkg'..."
|
||||||
if sudo yum install -y "$apt_pkg"; then
|
if sudo yum install -y "$apt_pkg"; then
|
||||||
log "✔ Successfully installed '$apt_pkg'."
|
log_success "Successfully installed '$apt_pkg'"
|
||||||
else
|
else
|
||||||
log "❌ ERROR: Failed to install '$apt_pkg'. Exiting."
|
log_error "Failed to install '$apt_pkg'"
|
||||||
exit 1
|
SCRIPT_EXIT_STATUS=1; return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_domain() {
|
validate_domain() { [[ "$1" =~ ^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$ ]]; }
|
||||||
local domain="$1"
|
validate_ip() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; }
|
||||||
if [[ "$domain" =~ ^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$ ]]; then
|
validate_email() { [[ "$1" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; }
|
||||||
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() {
|
create_default_backup() {
|
||||||
if [[ ! -f "$DEFAULT_CONF" ]]; then
|
if [[ ! -f "$DEFAULT_CONF" ]]; then
|
||||||
log "Creating initial backup of '$CONF_FILE' as '$DEFAULT_CONF'..."
|
log "Creating initial backup of '$CONF_FILE' as '$DEFAULT_CONF'..."
|
||||||
if sudo cp -a "$CONF_FILE" "$DEFAULT_CONF"; then
|
sudo cp -a "$CONF_FILE" "$DEFAULT_CONF" || {
|
||||||
log "✔ Initial backup created: '$DEFAULT_CONF'."
|
log_error "FATAL: Failed to create initial backup '$DEFAULT_CONF'"
|
||||||
else
|
SCRIPT_EXIT_STATUS=2; return 1
|
||||||
log "❌ FATAL: Failed to create initial backup '$DEFAULT_CONF'. Exiting."
|
}
|
||||||
exit 2
|
log_success "Created initial backup successfully"
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
log "Info: Default backup '$DEFAULT_CONF' already exists. Skipping creation."
|
log "Default backup '$DEFAULT_CONF' already exists. Skipping creation."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_xml() {
|
validate_xml() {
|
||||||
local file_to_validate="$1"
|
log_verbose "Validating XML structure of '$1'..."
|
||||||
log_verbose "Validating XML structure of '$file_to_validate'..."
|
sudo xmllint --noout "$1" 2>/dev/null || {
|
||||||
if sudo xmllint --noout "$file_to_validate" 2>/dev/null; then
|
log_error "Invalid XML structure in '$1'"
|
||||||
log_verbose "✔ XML structure of '$file_to_validate' is valid."
|
SCRIPT_EXIT_STATUS=1
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log "❌ ERROR: XML structure of '$file_to_validate' is invalid."
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
}
|
||||||
|
log_verbose "XML validation passed for '$1'"
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_dns() {
|
validate_dns() {
|
||||||
local domain="$1"
|
|
||||||
local expected_ip="$2"
|
|
||||||
log "Validating DNS resolution for '$domain'..."
|
|
||||||
local resolved_ip
|
local resolved_ip
|
||||||
resolved_ip=$(dig +short "$domain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)
|
resolved_ip=$(dig +short "$1" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1)
|
||||||
if [[ "$resolved_ip" == "$expected_ip" ]]; then
|
[[ "$resolved_ip" == "$2" ]]
|
||||||
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() {
|
validate_http_access() {
|
||||||
local domain="$1"
|
|
||||||
log "Validating HTTP access for '$domain'..."
|
|
||||||
local token
|
local token
|
||||||
token=$(openssl rand -hex 16)
|
token=$(openssl rand -hex 16)
|
||||||
echo "$token" > "/var/www/webroot/ROOT/.well-known/acme-challenge/test-token"
|
echo "$token" > "/var/www/webroot/ROOT/.well-known/acme-challenge/test-token"
|
||||||
local response
|
[[ "$(curl -s "http://$1/.well-known/acme-challenge/test-token")" == "$token" ]]
|
||||||
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() {
|
issue_certificate() {
|
||||||
local domain="$1"
|
if [[ -f "$CERT_DIR/$1/fullchain.pem" ]]; then
|
||||||
local email="$2"
|
log_success "Certificate already exists for '$1'. Skipping issuance."
|
||||||
log "Issuing SSL certificate for domain '$domain' with email '$email'..."
|
return
|
||||||
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
|
fi
|
||||||
|
log "Issuing SSL certificate for domain '$1' with email '$2'..."
|
||||||
|
sudo certbot certonly --standalone --preferred-challenges http -d "$1" --non-interactive --agree-tos --email "$2" || {
|
||||||
|
log_error "Failed to issue certificate for '$1'"
|
||||||
|
SCRIPT_EXIT_STATUS=1; return 1
|
||||||
|
}
|
||||||
|
log_success "Certificate successfully issued for '$1'"
|
||||||
}
|
}
|
||||||
|
|
||||||
update_httpd_config() {
|
update_httpd_config() {
|
||||||
local domain="$1"
|
local domain="$1"
|
||||||
local ip="$2"
|
local ip="$2"
|
||||||
|
local listener_name="HTTPS-$domain"
|
||||||
|
local vhost_name="Jelastic"
|
||||||
|
|
||||||
|
log "Checking if listener exists for '$listener_name'..."
|
||||||
|
local existing
|
||||||
|
existing=$(xmlstarlet sel -t -v "/httpServerConfig/listenerList/listener[name='$listener_name']/name" "$CONF_FILE" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -n "$existing" ]]; then
|
||||||
|
log "Listener '$listener_name' already exists. Checking for vhost mapping..."
|
||||||
|
local vhost_map_exists
|
||||||
|
vhost_map_exists=$(xmlstarlet sel -t -v "/httpServerConfig/listenerList/listener[name='$listener_name']/vhostMapList/vhostMap/domain" "$CONF_FILE" 2>/dev/null || true)
|
||||||
|
if [[ -z "$vhost_map_exists" ]]; then
|
||||||
|
log "Adding vhostMapList to existing listener for '$listener_name'..."
|
||||||
|
sudo cp -a "$CONF_FILE" "$BACKUP_FILE"
|
||||||
|
|
||||||
|
# Create a temporary XML file with the new vhostMapList
|
||||||
|
local temp_xml=$(mktemp)
|
||||||
|
cat > "$temp_xml" << EOF
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<vhostMapList>
|
||||||
|
<vhostMap>
|
||||||
|
<vhost>$vhost_name</vhost>
|
||||||
|
<domain>$domain</domain>
|
||||||
|
</vhostMap>
|
||||||
|
</vhostMapList>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# First validate the temporary XML
|
||||||
|
if ! xmllint --noout "$temp_xml" 2>/dev/null; then
|
||||||
|
log_error "Invalid temporary XML structure"
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Insert the vhostMapList into the existing listener
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-i "/httpServerConfig/listenerList/listener[name='$listener_name']" \
|
||||||
|
-t elem -n "vhostMapList" \
|
||||||
|
-v "$(xmlstarlet sel -t -c "//vhostMapList/*" "$temp_xml")" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to insert vhostMapList"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
|
||||||
|
# Validate the modified config
|
||||||
|
if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then
|
||||||
|
log_error "Invalid XML structure after modification. Restoring backup..."
|
||||||
|
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_success "Successfully added vhostMapList to existing listener"
|
||||||
|
else
|
||||||
|
log_success "vhostMap already present. No update needed."
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
log "Updating httpd_config.xml for domain '$domain' with IP '$ip'..."
|
log "Updating httpd_config.xml for domain '$domain' with IP '$ip'..."
|
||||||
|
sudo cp -a "$CONF_FILE" "$BACKUP_FILE"
|
||||||
|
|
||||||
# Create a backup of the current configuration
|
# Create a temporary XML file with the new listener configuration
|
||||||
log "Creating timestamped backup of '$CONF_FILE' as '$BACKUP_FILE'..."
|
local temp_xml=$(mktemp)
|
||||||
if sudo cp -a "$CONF_FILE" "$BACKUP_FILE"; then
|
cat > "$temp_xml" << EOF
|
||||||
log "✔ Backup created: '$BACKUP_FILE'."
|
<?xml version="1.0"?>
|
||||||
else
|
<listener>
|
||||||
log "❌ ERROR: Failed to create backup of '$CONF_FILE'. Exiting."
|
<name>$listener_name</name>
|
||||||
exit 2
|
<address>*:443</address>
|
||||||
|
<secure>1</secure>
|
||||||
|
<keyFile>/etc/letsencrypt/live/$domain/privkey.pem</keyFile>
|
||||||
|
<certFile>/etc/letsencrypt/live/$domain/fullchain.pem</certFile>
|
||||||
|
<vhostMapList>
|
||||||
|
<vhostMap>
|
||||||
|
<vhost>$vhost_name</vhost>
|
||||||
|
<domain>$domain</domain>
|
||||||
|
</vhostMap>
|
||||||
|
</vhostMapList>
|
||||||
|
</listener>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# First validate the temporary XML
|
||||||
|
if ! xmllint --noout "$temp_xml" 2>/dev/null; then
|
||||||
|
log_error "Invalid temporary XML structure"
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update the listener configuration using xmlstarlet
|
# Insert the new listener into the configuration using a different approach
|
||||||
log "Adding listener for domain '$domain' with IP '$ip'..."
|
|
||||||
sudo xmlstarlet ed -L \
|
sudo xmlstarlet ed -L \
|
||||||
-s "//listenerList" -t elem -n "listener" \
|
-s "//listenerList" \
|
||||||
-s "//listener[last()]" -t elem -n "name" -v "$domain" \
|
-t elem -n "listener" \
|
||||||
-s "//listener[last()]" -t elem -n "address" -v "$ip:443" \
|
"$CONF_FILE" || {
|
||||||
-s "//listener[last()]" -t elem -n "secure" -v "1" \
|
rm -f "$temp_xml"
|
||||||
-s "//listener[last()]" -t elem -n "keyFile" -v "/etc/letsencrypt/live/$domain/privkey.pem" \
|
log_error "Failed to create new listener element"
|
||||||
-s "//listener[last()]" -t elem -n "certFile" -v "/etc/letsencrypt/live/$domain/fullchain.pem" \
|
SCRIPT_EXIT_STATUS=1
|
||||||
"$CONF_FILE"
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# Validate the updated configuration
|
# Now add each element individually
|
||||||
if validate_xml "$CONF_FILE"; then
|
sudo xmlstarlet ed -L \
|
||||||
log "✔ Updated configuration is valid."
|
-s "//listenerList/listener[last()]" \
|
||||||
else
|
-t elem -n "name" \
|
||||||
log "❌ ERROR: Updated configuration is invalid. Restoring backup..."
|
-v "$listener_name" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add name element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-s "//listenerList/listener[last()]" \
|
||||||
|
-t elem -n "address" \
|
||||||
|
-v "*:443" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add address element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-s "//listenerList/listener[last()]" \
|
||||||
|
-t elem -n "secure" \
|
||||||
|
-v "1" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add secure element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-s "//listenerList/listener[last()]" \
|
||||||
|
-t elem -n "keyFile" \
|
||||||
|
-v "/etc/letsencrypt/live/$domain/privkey.pem" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add keyFile element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-s "//listenerList/listener[last()]" \
|
||||||
|
-t elem -n "certFile" \
|
||||||
|
-v "/etc/letsencrypt/live/$domain/fullchain.pem" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add certFile element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add vhostMapList and its children
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-s "//listenerList/listener[last()]" \
|
||||||
|
-t elem -n "vhostMapList" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add vhostMapList element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-s "//listenerList/listener[last()]/vhostMapList" \
|
||||||
|
-t elem -n "vhostMap" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add vhostMap element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-s "//listenerList/listener[last()]/vhostMapList/vhostMap" \
|
||||||
|
-t elem -n "vhost" \
|
||||||
|
-v "$vhost_name" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add vhost element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-s "//listenerList/listener[last()]/vhostMapList/vhostMap" \
|
||||||
|
-t elem -n "domain" \
|
||||||
|
-v "$domain" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
log_error "Failed to add domain element"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -f "$temp_xml"
|
||||||
|
|
||||||
|
# Validate the modified config
|
||||||
|
if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then
|
||||||
|
log_error "Invalid XML structure after modification. Restoring backup..."
|
||||||
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
|
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
|
||||||
log "✔ Backup restored: '$CONF_FILE'."
|
SCRIPT_EXIT_STATUS=1
|
||||||
exit 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log_success "Listener + vhostMap added for '$listener_name'"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_xml() {
|
||||||
|
local domain="$1"
|
||||||
|
log "Cleaning up XML structure..."
|
||||||
|
sudo cp -a "$CONF_FILE" "$BACKUP_FILE"
|
||||||
|
|
||||||
|
# Remove any incorrectly placed vhostMapList elements
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-d "//listenerList/vhostMapList[not(parent::listener)]" \
|
||||||
|
"$CONF_FILE" || {
|
||||||
|
log_error "Failed to clean up XML structure"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate the cleaned config
|
||||||
|
if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then
|
||||||
|
log_error "Invalid XML structure after cleanup. Restoring backup..."
|
||||||
|
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
|
||||||
|
SCRIPT_EXIT_STATUS=1
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_success "XML structure cleaned up successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
restart_litespeed() {
|
restart_litespeed() {
|
||||||
log "Restarting LiteSpeed server..."
|
log "Restarting LiteSpeed server..."
|
||||||
if sudo systemctl restart lsws; then
|
sudo systemctl restart lsws || {
|
||||||
log "✔ LiteSpeed server restarted successfully."
|
log_error "Failed to restart LiteSpeed. Restoring backup..."
|
||||||
else
|
|
||||||
log "❌ ERROR: Failed to restart LiteSpeed server. Restoring backup..."
|
|
||||||
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
|
sudo cp -a "$BACKUP_FILE" "$CONF_FILE"
|
||||||
log "✔ Backup restored: '$CONF_FILE'."
|
SCRIPT_EXIT_STATUS=3
|
||||||
exit 3
|
return 1
|
||||||
fi
|
}
|
||||||
|
log_success "LiteSpeed server restarted successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
# === Main Script Logic ===
|
# === Main Script Logic ===
|
||||||
log "Starting SSL Manager V2.0.1..."
|
main() {
|
||||||
|
# Setup logging first
|
||||||
|
setup_logging
|
||||||
|
log "Starting SSL Manager V2.0.4"
|
||||||
|
|
||||||
# Ensure log directory exists
|
# Parse parameters
|
||||||
sudo mkdir -p "$LOG_DIR" || { log "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
|
for arg in "$@"; do
|
||||||
sudo touch "$SCRIPT_LOG" "$XML_ERROR_LOG"
|
case $arg in
|
||||||
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)."
|
--public-ip=*) PUBLIC_IP="${arg#*=}"; log_verbose "Set public IP: $PUBLIC_IP";;
|
||||||
|
--domain=*) PRIMARY_DOMAIN="${arg#*=}"; DOMAINS=("$PRIMARY_DOMAIN"); log_verbose "Set primary domain: $PRIMARY_DOMAIN";;
|
||||||
|
--domains=*) IFS=',' read -ra DOMAINS <<< "${arg#*=}"; PRIMARY_DOMAIN="${DOMAINS[0]}"; log_verbose "Set domains: ${DOMAINS[*]}";;
|
||||||
|
--email=*) EMAIL="${arg#*=}"; log_verbose "Set email: $EMAIL";;
|
||||||
|
--verbose) VERBOSE=1; log "Verbose mode enabled";;
|
||||||
|
*) log_error "Invalid argument: $arg"; SCRIPT_EXIT_STATUS=1; exit 1;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# Argument Parsing
|
[[ -z "$PRIMARY_DOMAIN" || -z "$PUBLIC_IP" || -z "$EMAIL" ]] && {
|
||||||
for arg in "$@"; do
|
log_error "Missing required parameters. Provide --domains, --public-ip, and --email."
|
||||||
case $arg in
|
SCRIPT_EXIT_STATUS=1; exit 1
|
||||||
--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
|
validate_domain "$PRIMARY_DOMAIN" || { log_error "Invalid domain '$PRIMARY_DOMAIN'"; SCRIPT_EXIT_STATUS=1; exit 1; }
|
||||||
if [[ -z "$PRIMARY_DOMAIN" || -z "$PUBLIC_IP" || -z "$EMAIL" ]]; then
|
validate_ip "$PUBLIC_IP" || { log_error "Invalid IP '$PUBLIC_IP'"; SCRIPT_EXIT_STATUS=1; exit 1; }
|
||||||
log "❌ ERROR: Missing required parameters. Provide --domains, --public-ip, and --email."
|
validate_email "$EMAIL" || { log_error "Invalid email '$EMAIL'"; SCRIPT_EXIT_STATUS=1; exit 1; }
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! validate_domain "$PRIMARY_DOMAIN"; then
|
validate_dns "$PRIMARY_DOMAIN" "$PUBLIC_IP" || { log_error "DNS validation failed"; SCRIPT_EXIT_STATUS=1; exit 1; }
|
||||||
log "❌ ERROR: Invalid domain '$PRIMARY_DOMAIN'."
|
validate_http_access "$PRIMARY_DOMAIN" || { log_error "HTTP access validation failed"; SCRIPT_EXIT_STATUS=1; exit 1; }
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! validate_ip "$PUBLIC_IP"; then
|
log_verbose "Checking dependencies..."
|
||||||
log "❌ ERROR: Invalid IP address '$PUBLIC_IP'."
|
check_command xmllint libxml2-utils
|
||||||
exit 1
|
check_command xmlstarlet xmlstarlet
|
||||||
fi
|
check_command certbot certbot
|
||||||
|
|
||||||
if ! validate_email "$EMAIL"; then
|
create_default_backup
|
||||||
log "❌ ERROR: Invalid email address '$EMAIL'."
|
issue_certificate "$PRIMARY_DOMAIN" "$EMAIL"
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate DNS and HTTP Access
|
for domain in "${DOMAINS[@]}"; do
|
||||||
if ! validate_dns "$PRIMARY_DOMAIN" "$PUBLIC_IP"; then
|
update_httpd_config "$domain" "$PUBLIC_IP"
|
||||||
log "❌ ERROR: DNS validation failed. Exiting."
|
cleanup_xml "$domain"
|
||||||
exit 1
|
done
|
||||||
fi
|
|
||||||
|
|
||||||
if ! validate_http_access "$PRIMARY_DOMAIN"; then
|
restart_litespeed
|
||||||
log "❌ ERROR: HTTP access validation failed. Exiting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Dependency Checks
|
log_success "SSL Manager completed successfully"
|
||||||
log_verbose "Checking dependencies..."
|
SCRIPT_EXIT_STATUS=0
|
||||||
check_command "xmllint" "libxml2-utils"
|
}
|
||||||
check_command "xmlstarlet" "xmlstarlet"
|
|
||||||
check_command "certbot" "certbot"
|
|
||||||
|
|
||||||
# Create Default Backup
|
# === Entry Point ===
|
||||||
create_default_backup
|
main "$@"
|
||||||
|
|
||||||
# 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
|
|
|
@ -20,15 +20,65 @@ BACKUP_DIR="/var/www/conf/backups"
|
||||||
LOG_DIR="/var/log/mb-ssl"
|
LOG_DIR="/var/log/mb-ssl"
|
||||||
CERT_DIR="/etc/letsencrypt/live"
|
CERT_DIR="/etc/letsencrypt/live"
|
||||||
SCRIPT_LOG="${LOG_DIR}/ssl-remover.log"
|
SCRIPT_LOG="${LOG_DIR}/ssl-remover.log"
|
||||||
|
ERROR_LOG="${LOG_DIR}/ssl-remover-error.log"
|
||||||
|
DEBUG_LOG="${LOG_DIR}/ssl-remover-debug.log"
|
||||||
VERBOSE=0
|
VERBOSE=0
|
||||||
|
|
||||||
# === Functions ===
|
# === Functions ===
|
||||||
|
setup_logging() {
|
||||||
|
# Create log directory if it doesn't exist
|
||||||
|
sudo mkdir -p "$LOG_DIR" || { echo "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
sudo chown -R "$(whoami)":"$(id -gn)" "$LOG_DIR"
|
||||||
|
sudo chmod 755 "$LOG_DIR"
|
||||||
|
|
||||||
|
# Create log files with proper permissions
|
||||||
|
touch "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"
|
||||||
|
chmod 644 "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"
|
||||||
|
|
||||||
|
# Add log rotation if log files are too large (>10MB)
|
||||||
|
for log_file in "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"; do
|
||||||
|
if [ -f "$log_file" ] && [ "$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file")" -gt 10485760 ]; then
|
||||||
|
mv "$log_file" "${log_file}.$(date +%Y%m%d)"
|
||||||
|
touch "$log_file"
|
||||||
|
chmod 644 "$log_file"
|
||||||
|
gzip "${log_file}.$(date +%Y%m%d)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$SCRIPT_LOG"
|
local level="INFO"
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Log to main log file
|
||||||
|
echo "[$timestamp] [$level] $message" | tee -a "$SCRIPT_LOG"
|
||||||
|
|
||||||
|
# Log errors to error log file
|
||||||
|
if [[ "$message" == *"ERROR"* ]] || [[ "$message" == *"❌"* ]]; then
|
||||||
|
echo "[$timestamp] [$level] $message" >> "$ERROR_LOG"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
log_verbose() {
|
log_verbose() {
|
||||||
[[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
|
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [DEBUG] $1" | tee -a "$DEBUG_LOG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [ERROR] $message" | tee -a "$ERROR_LOG" "$SCRIPT_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [SUCCESS] $message" | tee -a "$SCRIPT_LOG"
|
||||||
}
|
}
|
||||||
|
|
||||||
send_email() {
|
send_email() {
|
||||||
|
@ -80,12 +130,15 @@ remove_certificate() {
|
||||||
log "Checking for certificate for domain '$domain'..."
|
log "Checking for certificate for domain '$domain'..."
|
||||||
|
|
||||||
if certbot certificates | grep -q "Domains: $domain"; then
|
if certbot certificates | grep -q "Domains: $domain"; then
|
||||||
log "Removing certificate for '$domain'..."
|
log "Found certificate for '$domain'. Proceeding with removal..."
|
||||||
if certbot delete --cert-name "$domain" --non-interactive; then
|
if certbot delete --cert-name "$domain" --non-interactive; then
|
||||||
|
# Remove all certificate files
|
||||||
rm -rf "/etc/letsencrypt/live/$domain"*
|
rm -rf "/etc/letsencrypt/live/$domain"*
|
||||||
log "Certificate successfully removed for '$domain'."
|
rm -rf "/etc/letsencrypt/archive/$domain"*
|
||||||
|
log_success "Certificate successfully removed for '$domain'"
|
||||||
|
log_verbose "Removed certificate files from /etc/letsencrypt/live/$domain and /etc/letsencrypt/archive/$domain"
|
||||||
else
|
else
|
||||||
log "❌ ERROR: Failed to remove certificate for '$domain'."
|
log_error "Failed to remove certificate for '$domain'"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
@ -95,28 +148,56 @@ remove_certificate() {
|
||||||
|
|
||||||
cleanup_listeners() {
|
cleanup_listeners() {
|
||||||
local domain="$1"
|
local domain="$1"
|
||||||
log "Cleaning up listeners and configurations for '$domain'..."
|
local listener_name="HTTPS-$domain"
|
||||||
|
log "Starting cleanup for listener '$listener_name'..."
|
||||||
|
|
||||||
# Remove listener for the domain
|
# First backup the config
|
||||||
sudo xmlstarlet ed -L \
|
local backup_file="${BACKUP_DIR}/httpd_config.pre-removal-$(date +%Y%m%d%H%M%S).xml"
|
||||||
-d "//listener[name='HTTPS-$domain']" \
|
cp "$CONF_FILE" "$backup_file" || {
|
||||||
"$CONF_FILE"
|
log_error "Failed to create backup before cleanup"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
log_verbose "Created backup at: $backup_file"
|
||||||
|
|
||||||
# Remove vhostMap entries
|
# Remove the entire listener element with all its children
|
||||||
sudo xmlstarlet ed -L \
|
sudo xmlstarlet ed -L \
|
||||||
-d "//vhostMap[domain='$domain']" \
|
-d "//listenerList/listener[name='$listener_name']" \
|
||||||
"$CONF_FILE"
|
"$CONF_FILE" || {
|
||||||
|
log_error "Failed to remove listener '$listener_name'"
|
||||||
|
cp "$backup_file" "$CONF_FILE"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
log_verbose "Removed listener element: $listener_name"
|
||||||
|
|
||||||
# Remove related virtual host
|
# Validate XML after removal
|
||||||
local vhost_name="${domain//./_}"
|
if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then
|
||||||
sudo xmlstarlet ed -L \
|
log_error "Invalid XML structure after listener removal. Restoring backup..."
|
||||||
-d "//virtualHost[name='$vhost_name']" \
|
cp "$backup_file" "$CONF_FILE"
|
||||||
"$CONF_FILE"
|
return 1
|
||||||
|
fi
|
||||||
|
log_verbose "XML validation passed after listener removal"
|
||||||
|
|
||||||
# Cleanup empty listenerList tags
|
# Clean up any orphaned vhostMapList elements
|
||||||
sudo xmlstarlet ed -L \
|
sudo xmlstarlet ed -L \
|
||||||
-d "//listenerList[count(*)=0]" \
|
-d "//vhostMapList[not(parent::listener)]" \
|
||||||
"$CONF_FILE"
|
"$CONF_FILE" || {
|
||||||
|
log_error "Failed to clean up orphaned vhostMapList"
|
||||||
|
cp "$backup_file" "$CONF_FILE"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
log_verbose "Cleaned up orphaned vhostMapList elements"
|
||||||
|
|
||||||
|
# Validate XML after cleanup
|
||||||
|
if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then
|
||||||
|
log_error "Invalid XML structure after cleanup. Restoring backup..."
|
||||||
|
cp "$backup_file" "$CONF_FILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_verbose "XML validation passed after cleanup"
|
||||||
|
|
||||||
|
log_success "Successfully removed listener '$listener_name' and cleaned up XML structure"
|
||||||
|
rm -f "$backup_file"
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_xml() {
|
validate_xml() {
|
||||||
|
@ -144,21 +225,27 @@ main() {
|
||||||
declare -a DOMAINS
|
declare -a DOMAINS
|
||||||
EMAIL=""
|
EMAIL=""
|
||||||
|
|
||||||
|
# Setup logging first
|
||||||
|
setup_logging
|
||||||
|
log "Starting SSL Removal Process"
|
||||||
|
|
||||||
# Parse parameters
|
# Parse parameters
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--domains=*)
|
--domains=*)
|
||||||
IFS=',' read -ra DOMAINS <<< "${1#*=}"
|
IFS=',' read -ra DOMAINS <<< "${1#*=}"
|
||||||
|
log_verbose "Parsed domains: ${DOMAINS[*]}"
|
||||||
;;
|
;;
|
||||||
--email=*)
|
--email=*)
|
||||||
EMAIL="${1#*=}"
|
EMAIL="${1#*=}"
|
||||||
|
log_verbose "Set email notification to: $EMAIL"
|
||||||
;;
|
;;
|
||||||
--verbose)
|
--verbose)
|
||||||
VERBOSE=1
|
VERBOSE=1
|
||||||
log "Verbose mode enabled."
|
log "Verbose mode enabled"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log "Invalid parameter: $1"
|
log_error "Invalid parameter: $1"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
@ -167,40 +254,39 @@ main() {
|
||||||
|
|
||||||
# Validate input
|
# Validate input
|
||||||
if [[ ${#DOMAINS[@]} -eq 0 ]]; then
|
if [[ ${#DOMAINS[@]} -eq 0 ]]; then
|
||||||
log "❌ ERROR: --domains parameter is required."
|
log_error "--domains parameter is required"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure log directory exists
|
|
||||||
mkdir -p "$LOG_DIR" || { log "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
|
|
||||||
touch "$SCRIPT_LOG"
|
|
||||||
chmod 0644 "$SCRIPT_LOG"
|
|
||||||
|
|
||||||
# Backup configuration
|
|
||||||
backup_config
|
|
||||||
|
|
||||||
# Process each domain
|
# Process each domain
|
||||||
for domain in "${DOMAINS[@]}"; do
|
for domain in "${DOMAINS[@]}"; do
|
||||||
log "Processing domain: $domain"
|
log "Processing domain: $domain"
|
||||||
|
|
||||||
# Validate domain format
|
# Validate domain format
|
||||||
if ! validate_domain "$domain"; then
|
if ! validate_domain "$domain"; then
|
||||||
log "❌ ERROR: Invalid domain '$domain'. Skipping."
|
log_error "Invalid domain '$domain'. Skipping."
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove certificate
|
# Remove certificate first
|
||||||
remove_certificate "$domain"
|
if ! remove_certificate "$domain"; then
|
||||||
|
log "⚠ Warning: Failed to remove certificate for '$domain'. Continuing with cleanup..."
|
||||||
|
fi
|
||||||
|
|
||||||
# Clean up listeners and configurations
|
# Clean up listeners and configurations
|
||||||
cleanup_listeners "$domain"
|
if ! cleanup_listeners "$domain"; then
|
||||||
|
log_error "Failed to clean up listeners for '$domain'"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Validate XML configuration
|
# Validate final XML configuration
|
||||||
if validate_xml; then
|
if validate_xml; then
|
||||||
restart_litespeed
|
restart_litespeed
|
||||||
|
log_success "SSL Removal completed successfully for domains: ${DOMAINS[*]}"
|
||||||
send_email "SSL Removal Complete" "Successfully removed SSL for domains: ${DOMAINS[*]}"
|
send_email "SSL Removal Complete" "Successfully removed SSL for domains: ${DOMAINS[*]}"
|
||||||
else
|
else
|
||||||
|
log_error "SSL removed but configuration validation failed for domains: ${DOMAINS[*]}"
|
||||||
send_email "SSL Removal Warning" "SSL removed but configuration validation failed for domains: ${DOMAINS[*]}"
|
send_email "SSL Removal Warning" "SSL removed but configuration validation failed for domains: ${DOMAINS[*]}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -22,13 +22,19 @@ SERVER_ROOT="/var/www" # Server root directory
|
||||||
LOG_DIR="/var/log/mb-ssl" # Log directory
|
LOG_DIR="/var/log/mb-ssl" # Log directory
|
||||||
MAJOR_CHANGE_THRESHOLD=20 # Lines changed to trigger "major recovery" warning
|
MAJOR_CHANGE_THRESHOLD=20 # Lines changed to trigger "major recovery" warning
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
SCRIPT_LOG="${LOG_DIR}/xmlchecker.log"
|
||||||
|
ERROR_LOG="${LOG_DIR}/xmlchecker-error.log"
|
||||||
|
DEBUG_LOG="${LOG_DIR}/xmlchecker-debug.log"
|
||||||
|
BACKUP_FILE="${LOG_DIR}/httpd_config_$(date +%Y%m%d_%H%M%S).bak" # Timestamped backup
|
||||||
|
|
||||||
|
# SSL Remover script path
|
||||||
|
SSL_REMOVER="/home/litespeed/mbmanager/ssl-manager/ssl_remover.sh"
|
||||||
|
|
||||||
# - Internal Variables -
|
# - Internal Variables -
|
||||||
TMP_FILE="/tmp/test_config_$(date +%s).xml" # Temporary copy of original/cleaned file
|
TMP_FILE="/tmp/test_config_$(date +%s).xml" # Temporary copy of original/cleaned file
|
||||||
RECOVERY_TMP_FILE="/tmp/recovery_test_config_$(date +%s).xml" # Holds output of xmllint --recover
|
RECOVERY_TMP_FILE="/tmp/recovery_test_config_$(date +%s).xml" # Holds output of xmllint --recover
|
||||||
MINIMAL_TMP="/tmp/minimal_config_$(date +%s).xml" # Temporary minimal config file
|
MINIMAL_TMP="/tmp/minimal_config_$(date +%s).xml" # Temporary minimal config file
|
||||||
BACKUP_FILE="${LOG_DIR}/httpd_config_$(date +%Y%m%d_%H%M%S).bak" # Timestamped backup
|
|
||||||
XML_ERROR_LOG="${LOG_DIR}/xml-parse-error.log"
|
|
||||||
SCRIPT_LOG="${LOG_DIR}/config_fixer.log"
|
|
||||||
VERBOSE=0
|
VERBOSE=0
|
||||||
INITIAL_FILE_VALID=0 # Flag: 1 if CONF_FILE was valid before fixes
|
INITIAL_FILE_VALID=0 # Flag: 1 if CONF_FILE was valid before fixes
|
||||||
RECOVERY_PRODUCED_VALID_XML=0 # Flag: 1 if xmllint --recover output was valid XML
|
RECOVERY_PRODUCED_VALID_XML=0 # Flag: 1 if xmllint --recover output was valid XML
|
||||||
|
@ -39,12 +45,60 @@ APPLIED_CONFIG_SOURCE="none" # Tracks what was finally applied
|
||||||
trap 'log "Script interrupted. Cleaning up temporary files..."; sudo rm -f "$TMP_FILE" "$RECOVERY_TMP_FILE" "$MINIMAL_TMP"' EXIT
|
trap 'log "Script interrupted. Cleaning up temporary files..."; sudo rm -f "$TMP_FILE" "$RECOVERY_TMP_FILE" "$MINIMAL_TMP"' EXIT
|
||||||
|
|
||||||
# === Functions ===
|
# === Functions ===
|
||||||
|
setup_logging() {
|
||||||
|
# Create log directory if it doesn't exist
|
||||||
|
sudo mkdir -p "$LOG_DIR" || { echo "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
sudo chown -R "$(whoami)":"$(id -gn)" "$LOG_DIR"
|
||||||
|
sudo chmod 755 "$LOG_DIR"
|
||||||
|
|
||||||
|
# Create log files with proper permissions
|
||||||
|
touch "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"
|
||||||
|
chmod 644 "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"
|
||||||
|
|
||||||
|
# Add log rotation if log files are too large (>10MB)
|
||||||
|
for log_file in "$SCRIPT_LOG" "$ERROR_LOG" "$DEBUG_LOG"; do
|
||||||
|
if [ -f "$log_file" ] && [ "$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file")" -gt 10485760 ]; then
|
||||||
|
mv "$log_file" "${log_file}.$(date +%Y%m%d)"
|
||||||
|
touch "$log_file"
|
||||||
|
chmod 644 "$log_file"
|
||||||
|
gzip "${log_file}.$(date +%Y%m%d)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | sudo tee -a "$SCRIPT_LOG"
|
local level="INFO"
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# Log to main log file
|
||||||
|
echo "[$timestamp] [$level] $message" | sudo tee -a "$SCRIPT_LOG"
|
||||||
|
|
||||||
|
# Log errors to error log file
|
||||||
|
if [[ "$message" == *"ERROR"* ]] || [[ "$message" == *"❌"* ]]; then
|
||||||
|
echo "[$timestamp] [$level] $message" | sudo tee -a "$ERROR_LOG"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
log_verbose() {
|
log_verbose() {
|
||||||
[[ "$VERBOSE" -eq 1 ]] && log "[VERBOSE] $1"
|
if [[ "$VERBOSE" -eq 1 ]]; then
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [DEBUG] $1" | sudo tee -a "$DEBUG_LOG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [ERROR] $message" | sudo tee -a "$ERROR_LOG" "$SCRIPT_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
local message="$1"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo "[$timestamp] [SUCCESS] $message" | sudo tee -a "$SCRIPT_LOG"
|
||||||
}
|
}
|
||||||
|
|
||||||
check_command() {
|
check_command() {
|
||||||
|
@ -61,6 +115,70 @@ check_command() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_listener_certificates() {
|
||||||
|
local file_to_check="$1"
|
||||||
|
log "Validating listener certificates in '$file_to_check'..."
|
||||||
|
local invalid_listeners=()
|
||||||
|
|
||||||
|
# Get all HTTPS listeners
|
||||||
|
local listeners
|
||||||
|
listeners=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener[contains(name, 'HTTPS-')]/name" "$file_to_check" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
for listener in $listeners; do
|
||||||
|
local domain="${listener#HTTPS-}"
|
||||||
|
local key_file=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener[name='$listener']/keyFile" "$file_to_check" 2>/dev/null || echo "")
|
||||||
|
local cert_file=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener[name='$listener']/certFile" "$file_to_check" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Check if certificate files exist
|
||||||
|
if [[ ! -f "$key_file" ]] || [[ ! -f "$cert_file" ]]; then
|
||||||
|
log "⚠ Found invalid listener '$listener' with missing certificate files"
|
||||||
|
log_verbose "Key file missing: $key_file"
|
||||||
|
log_verbose "Cert file missing: $cert_file"
|
||||||
|
invalid_listeners+=("$domain")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if certificate is still valid
|
||||||
|
if [[ -f "$cert_file" ]]; then
|
||||||
|
local cert_expiry
|
||||||
|
cert_expiry=$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2)
|
||||||
|
local expiry_date
|
||||||
|
expiry_date=$(date -d "$cert_expiry" +%s)
|
||||||
|
local current_date
|
||||||
|
current_date=$(date +%s)
|
||||||
|
|
||||||
|
if [[ $expiry_date -lt $current_date ]]; then
|
||||||
|
log "⚠ Found expired certificate for listener '$listener'"
|
||||||
|
log_verbose "Certificate expired on: $cert_expiry"
|
||||||
|
invalid_listeners+=("$domain")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# If we found invalid listeners, clean them up
|
||||||
|
if [[ ${#invalid_listeners[@]} -gt 0 ]]; then
|
||||||
|
log "Found ${#invalid_listeners[@]} invalid listener(s). Cleaning up..."
|
||||||
|
for domain in "${invalid_listeners[@]}"; do
|
||||||
|
log "Cleaning up invalid listener for domain: $domain"
|
||||||
|
if [[ -f "$SSL_REMOVER" ]]; then
|
||||||
|
log "Running SSL remover for domain: $domain"
|
||||||
|
sudo bash "$SSL_REMOVER" --domains="$domain" --verbose || {
|
||||||
|
log_error "Failed to clean up invalid listener for domain: $domain"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
log_error "SSL remover script not found at: $SSL_REMOVER"
|
||||||
|
# Fallback to manual cleanup
|
||||||
|
sudo xmlstarlet ed -L \
|
||||||
|
-d "//listenerList/listener[name='HTTPS-$domain']" \
|
||||||
|
"$file_to_check" || {
|
||||||
|
log_error "Failed to remove invalid listener for domain: $domain"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
return ${#invalid_listeners[@]}
|
||||||
|
}
|
||||||
|
|
||||||
perform_semantic_checks() {
|
perform_semantic_checks() {
|
||||||
local file_to_check="$1"
|
local file_to_check="$1"
|
||||||
log_verbose "Performing semantic checks on '$file_to_check'..."
|
log_verbose "Performing semantic checks on '$file_to_check'..."
|
||||||
|
@ -68,41 +186,25 @@ perform_semantic_checks() {
|
||||||
|
|
||||||
# Check for critical elements
|
# Check for critical elements
|
||||||
if ! sudo xmlstarlet sel -t -c "//virtualHostList" "$file_to_check" &>/dev/null; then
|
if ! sudo xmlstarlet sel -t -c "//virtualHostList" "$file_to_check" &>/dev/null; then
|
||||||
log "Warning: <virtualHostList> is missing in '$file_to_check'."
|
log_error "Missing <virtualHostList> in '$file_to_check'"
|
||||||
semantic_errors=$((semantic_errors + 1))
|
semantic_errors=$((semantic_errors + 1))
|
||||||
fi
|
fi
|
||||||
if ! sudo xmlstarlet sel -t -c "//listenerList" "$file_to_check" &>/dev/null; then
|
if ! sudo xmlstarlet sel -t -c "//listenerList" "$file_to_check" &>/dev/null; then
|
||||||
log "Warning: <listenerList> is missing in '$file_to_check'."
|
log_error "Missing <listenerList> in '$file_to_check'"
|
||||||
semantic_errors=$((semantic_errors + 1))
|
semantic_errors=$((semantic_errors + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Validate listener certificates
|
# Validate listener certificates
|
||||||
local listeners
|
if ! validate_listener_certificates "$file_to_check"; then
|
||||||
listeners=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener/name" "$file_to_check" 2>/dev/null || echo "")
|
semantic_errors=$((semantic_errors + 1))
|
||||||
for listener in $listeners; do
|
fi
|
||||||
local key_file=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener[name='$listener']/keyFile" "$file_to_check" 2>/dev/null || echo "")
|
|
||||||
local cert_file=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener[name='$listener']/certFile" "$file_to_check" 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
# Resolve paths
|
|
||||||
key_file=$(echo "$key_file" | sed "s|\$SERVER_ROOT|$SERVER_ROOT|g")
|
|
||||||
cert_file=$(echo "$cert_file" | sed "s|\$SERVER_ROOT|$SERVER_ROOT|g")
|
|
||||||
|
|
||||||
if [[ -n "$key_file" && ! -e "$key_file" ]]; then
|
|
||||||
log "Warning: Listener '$listener' has invalid keyFile path: '$key_file'"
|
|
||||||
semantic_errors=$((semantic_errors + 1))
|
|
||||||
fi
|
|
||||||
if [[ -n "$cert_file" && ! -e "$cert_file" ]]; then
|
|
||||||
log "Warning: Listener '$listener' has invalid certFile path: '$cert_file'"
|
|
||||||
semantic_errors=$((semantic_errors + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Return result
|
# Return result
|
||||||
if [[ "$semantic_errors" -eq 0 ]]; then
|
if [[ "$semantic_errors" -eq 0 ]]; then
|
||||||
log_verbose "Semantic checks passed for '$file_to_check'."
|
log_success "Semantic checks passed for '$file_to_check'"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
log "⚠ Semantic Check: Found $semantic_errors potential issue(s) in '$file_to_check'. Review warnings."
|
log_error "Found $semantic_errors semantic issue(s) in '$file_to_check'"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
@ -189,84 +291,88 @@ apply_and_restart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# === Main Script Logic ===
|
# === Main Script Logic ===
|
||||||
log "Starting XML config check/fix V1.8.0 for $CONF_FILE"
|
main() {
|
||||||
sudo mkdir -p "$LOG_DIR" || { log "❌ ERROR: Cannot create log directory '$LOG_DIR'. Check permissions."; exit 1; }
|
# Setup logging first
|
||||||
sudo touch "$SCRIPT_LOG" "$XML_ERROR_LOG"
|
setup_logging
|
||||||
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)."
|
log "Starting XML config check/fix V1.8.0 for $CONF_FILE"
|
||||||
|
|
||||||
# Argument Parsing
|
# Argument Parsing
|
||||||
if [[ "${1:-}" == "--verbose" ]]; then
|
if [[ "${1:-}" == "--verbose" ]]; then
|
||||||
VERBOSE=1
|
VERBOSE=1
|
||||||
log "Verbose mode enabled."
|
log "Verbose mode enabled"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Dependency Checks
|
# Dependency Checks
|
||||||
log_verbose "Checking dependencies..."
|
log_verbose "Checking dependencies..."
|
||||||
check_command "xmllint" "libxml2-utils"
|
check_command "xmllint" "libxml2-utils"
|
||||||
check_command "xmlstarlet" "xmlstarlet"
|
check_command "xmlstarlet" "xmlstarlet"
|
||||||
|
|
||||||
# Backup Original File
|
# Backup Original File
|
||||||
log "Backing up '$CONF_FILE' to '$BACKUP_FILE'..."
|
log "Backing up '$CONF_FILE' to '$BACKUP_FILE'..."
|
||||||
sudo cp -a "$CONF_FILE" "$BACKUP_FILE"
|
sudo cp -a "$CONF_FILE" "$BACKUP_FILE"
|
||||||
|
|
||||||
# Initial Validation
|
# Initial Validation
|
||||||
log_verbose "Running initial xmllint validation on '$CONF_FILE'..."
|
log_verbose "Running initial xmllint validation on '$CONF_FILE'..."
|
||||||
if sudo xmllint --noout "$CONF_FILE" 2>"$XML_ERROR_LOG"; then
|
if sudo xmllint --noout "$CONF_FILE" 2>/dev/null; then
|
||||||
log "✔ Initial Check: '$CONF_FILE' is valid XML."
|
log_success "Initial Check: '$CONF_FILE' is valid XML"
|
||||||
INITIAL_FILE_VALID=1
|
INITIAL_FILE_VALID=1
|
||||||
else
|
else
|
||||||
log "⚠ Initial Check: '$CONF_FILE' is invalid XML. Will attempt recovery/cleanup."
|
log_error "Initial Check: '$CONF_FILE' is invalid XML. Will attempt recovery/cleanup"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Recovery Attempt
|
# Recovery Attempt
|
||||||
if [[ "$INITIAL_FILE_VALID" -eq 0 ]]; then
|
if [[ "$INITIAL_FILE_VALID" -eq 0 ]]; then
|
||||||
log "Attempting automatic recovery with 'xmllint --recover'..."
|
log "Attempting automatic recovery with 'xmllint --recover'..."
|
||||||
sudo rm -f "$RECOVERY_TMP_FILE"
|
sudo rm -f "$RECOVERY_TMP_FILE"
|
||||||
if xmllint --recover "$CONF_FILE" 2>/dev/null > "$RECOVERY_TMP_FILE"; then
|
if xmllint --recover "$CONF_FILE" 2>/dev/null > "$RECOVERY_TMP_FILE"; then
|
||||||
if sudo xmllint --noout "$RECOVERY_TMP_FILE" 2>/dev/null; then
|
if sudo xmllint --noout "$RECOVERY_TMP_FILE" 2>/dev/null; then
|
||||||
log "✔ 'xmllint --recover' produced structurally valid XML output."
|
log_success "'xmllint --recover' produced structurally valid XML output"
|
||||||
RECOVERY_PRODUCED_VALID_XML=1
|
RECOVERY_PRODUCED_VALID_XML=1
|
||||||
|
else
|
||||||
|
log_error "'xmllint --recover' ran but the output is STILL invalid XML. Discarding recovery"
|
||||||
|
sudo rm -f "$RECOVERY_TMP_FILE"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
log "⚠ 'xmllint --recover' ran but the output is STILL invalid XML. Discarding recovery."
|
log_error "'xmllint --recover' command failed or produced errors. Discarding recovery"
|
||||||
sudo rm -f "$RECOVERY_TMP_FILE"
|
sudo rm -f "$RECOVERY_TMP_FILE"
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
log "⚠ 'xmllint --recover' command failed or produced errors. Discarding recovery."
|
|
||||||
sudo rm -f "$RECOVERY_TMP_FILE"
|
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
# Final Validation and Decision
|
# Final Validation and Decision
|
||||||
FINAL_CONFIG_SOURCE_FILE=""
|
FINAL_CONFIG_SOURCE_FILE=""
|
||||||
FINAL_CONFIG_SOURCE_DESC=""
|
FINAL_CONFIG_SOURCE_DESC=""
|
||||||
FINAL_EXIT_CODE=0
|
FINAL_EXIT_CODE=0
|
||||||
log_verbose "Running final validation and decision logic..."
|
log_verbose "Running final validation and decision logic..."
|
||||||
|
|
||||||
# Check 1: Is the original (potentially cleaned) temp file valid now?
|
# Check 1: Is the original (potentially cleaned) temp file valid now?
|
||||||
if [[ "$INITIAL_FILE_VALID" -eq 1 ]]; then
|
if [[ "$INITIAL_FILE_VALID" -eq 1 ]]; then
|
||||||
FINAL_CONFIG_SOURCE_FILE="$CONF_FILE"
|
FINAL_CONFIG_SOURCE_FILE="$CONF_FILE"
|
||||||
FINAL_CONFIG_SOURCE_DESC="original"
|
FINAL_CONFIG_SOURCE_DESC="original"
|
||||||
elif [[ "$RECOVERY_PRODUCED_VALID_XML" -eq 1 ]]; then
|
elif [[ "$RECOVERY_PRODUCED_VALID_XML" -eq 1 ]]; then
|
||||||
FINAL_CONFIG_SOURCE_FILE="$RECOVERY_TMP_FILE"
|
FINAL_CONFIG_SOURCE_FILE="$RECOVERY_TMP_FILE"
|
||||||
FINAL_CONFIG_SOURCE_DESC="recovered"
|
FINAL_CONFIG_SOURCE_DESC="recovered"
|
||||||
else
|
|
||||||
log "❌ FATAL: Neither original nor recovered file is valid. Falling back to default or minimal config."
|
|
||||||
FINAL_EXIT_CODE=2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Apply and Restart
|
|
||||||
if [[ -n "$FINAL_CONFIG_SOURCE_FILE" ]]; then
|
|
||||||
if apply_and_restart "$FINAL_CONFIG_SOURCE_FILE" "$FINAL_CONFIG_SOURCE_DESC"; then
|
|
||||||
log "✅ Script completed successfully. Applied: $FINAL_CONFIG_SOURCE_DESC."
|
|
||||||
exit "$FINAL_EXIT_CODE"
|
|
||||||
else
|
else
|
||||||
log "❌ FATAL: Failed to apply and restart using $FINAL_CONFIG_SOURCE_DESC config."
|
log_error "Neither original nor recovered file is valid. Falling back to default or minimal config"
|
||||||
|
FINAL_EXIT_CODE=2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply and Restart
|
||||||
|
if [[ -n "$FINAL_CONFIG_SOURCE_FILE" ]]; then
|
||||||
|
if apply_and_restart "$FINAL_CONFIG_SOURCE_FILE" "$FINAL_CONFIG_SOURCE_DESC"; then
|
||||||
|
log_success "Script completed successfully. Applied: $FINAL_CONFIG_SOURCE_DESC"
|
||||||
|
exit "$FINAL_EXIT_CODE"
|
||||||
|
else
|
||||||
|
log_error "Failed to apply and restart using $FINAL_CONFIG_SOURCE_DESC config"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_error "No valid configuration file found. Exiting"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
}
|
||||||
log "❌ FATAL: No valid configuration file found. Exiting."
|
|
||||||
exit 1
|
# === Entry Point ===
|
||||||
fi
|
main "$@"
|
||||||
|
|
||||||
# === Fallback Logic ===
|
# === Fallback Logic ===
|
||||||
log "Attempting fallback sequence..."
|
log "Attempting fallback sequence..."
|
||||||
|
|
Loading…
Reference in New Issue