From 8533225d006a0b3c4478895e6c8eb12199b7c93a Mon Sep 17 00:00:00 2001 From: Anthony Date: Fri, 28 Mar 2025 01:09:16 +0800 Subject: [PATCH] Added xmlstarlet installation --- scripts/ssl-manager/ssl_remover.sh | 377 ++++++++++++++++++++++++----- scripts/ssl-manager/xmlchecker.sh | 27 ++- 2 files changed, 343 insertions(+), 61 deletions(-) diff --git a/scripts/ssl-manager/ssl_remover.sh b/scripts/ssl-manager/ssl_remover.sh index 684bb1d..84ddf06 100644 --- a/scripts/ssl-manager/ssl_remover.sh +++ b/scripts/ssl-manager/ssl_remover.sh @@ -24,7 +24,53 @@ ERROR_LOG="${LOG_DIR}/ssl-remover-error.log" DEBUG_LOG="${LOG_DIR}/ssl-remover-debug.log" VERBOSE=0 +# Validate configuration file exists and is readable +if [[ ! -f "$CONF_FILE" ]]; then + echo "❌ ERROR: Configuration file '$CONF_FILE' does not exist" + exit 1 +fi + +if [[ ! -r "$CONF_FILE" ]]; then + echo "❌ ERROR: Configuration file '$CONF_FILE' is not readable" + exit 1 +fi + +# Validate required tools are available +for tool in xmlstarlet xmllint certbot; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "❌ ERROR: Required tool '$tool' is not installed" + exit 1 + fi +done + # === Functions === +check_command() { + local cmd="$1" + local pkg="$2" + if ! command -v "$cmd" &>/dev/null; then + log "⚠ Required command '$cmd' not found. Attempting to install package '$pkg'..." + + # Try dnf first (AlmaLinux) + if command -v dnf &>/dev/null; then + if sudo dnf install -y "$pkg"; then + log "✔ Successfully installed '$pkg' using dnf." + return 0 + fi + fi + + # Fallback to yum (CentOS) + if command -v yum &>/dev/null; then + if sudo yum install -y "$pkg"; then + log "✔ Successfully installed '$pkg' using yum." + return 0 + fi + fi + + log "❌ ERROR: Failed to install '$pkg' using either dnf or yum. Exiting." + exit 1 + fi +} + 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; } @@ -116,11 +162,15 @@ backup_config() { local timestamp=$(date +%Y%m%d%H%M%S) local backup_file="${BACKUP_DIR}/httpd_config.pre-removal-${timestamp}.xml" - mkdir -p "$BACKUP_DIR" + # Create backup directory with proper permissions + sudo mkdir -p "$BACKUP_DIR" || { log_error "Failed to create backup directory '$BACKUP_DIR'"; exit 2; } + sudo chown -R "$(whoami)":"$(id -gn)" "$BACKUP_DIR" + sudo chmod 755 "$BACKUP_DIR" + if cp "$CONF_FILE" "$backup_file"; then log "Config backup saved to $backup_file" else - log "❌ ERROR: Failed to create backup '$backup_file'. Exiting." + log_error "Failed to create backup '$backup_file'. Exiting." exit 2 fi } @@ -129,21 +179,68 @@ remove_certificate() { local domain="$1" log "Checking for certificate for domain '$domain'..." - if certbot certificates | grep -q "Domains: $domain"; then - log "Found certificate for '$domain'. Proceeding with removal..." - if certbot delete --cert-name "$domain" --non-interactive; then - # Remove all certificate files - rm -rf "/etc/letsencrypt/live/$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 - log_error "Failed to remove certificate for '$domain'" - return 1 - fi - else - log "No certificate found for '$domain'. Skipping removal." + # Validate domain format first + if ! validate_domain "$domain"; then + log_error "Invalid domain format: '$domain'" + return 1 fi + + # Check if certbot is available and working + if ! certbot --version >/dev/null 2>&1; then + log_error "certbot is not available or not working properly" + return 1 + fi + + # Check for certificate existence with better error handling + if ! certbot certificates | grep -q "Domains: $domain"; then + log "No certificate found for '$domain'. Skipping removal." + return 0 + fi + + log "Found certificate for '$domain'. Proceeding with removal..." + + # Create backup of certificate files before removal + local cert_backup_dir="${BACKUP_DIR}/certificates/${domain}_$(date +%Y%m%d%H%M%S)" + sudo mkdir -p "$cert_backup_dir" + + if [[ -d "/etc/letsencrypt/live/$domain" ]]; then + sudo cp -r "/etc/letsencrypt/live/$domain" "$cert_backup_dir/" + fi + if [[ -d "/etc/letsencrypt/archive/$domain" ]]; then + sudo cp -r "/etc/letsencrypt/archive/$domain" "$cert_backup_dir/" + fi + + # Attempt to remove certificate + if ! certbot delete --cert-name "$domain" --non-interactive; then + log_error "Failed to remove certificate for '$domain'" + return 1 + fi + + # Verify certificate removal + if certbot certificates | grep -q "Domains: $domain"; then + log_error "Certificate removal verification failed for '$domain'" + return 1 + fi + + # Remove certificate files with verification + local files_removed=0 + if [[ -d "/etc/letsencrypt/live/$domain" ]]; then + sudo rm -rf "/etc/letsencrypt/live/$domain"* + ((files_removed++)) + fi + if [[ -d "/etc/letsencrypt/archive/$domain" ]]; then + sudo rm -rf "/etc/letsencrypt/archive/$domain"* + ((files_removed++)) + fi + + if [[ $files_removed -eq 0 ]]; then + log "No certificate files found to remove for '$domain'" + else + log_success "Certificate successfully removed for '$domain'" + log_verbose "Removed certificate files from /etc/letsencrypt/live/$domain and /etc/letsencrypt/archive/$domain" + fi + + return 0 } cleanup_listeners() { @@ -151,6 +248,17 @@ cleanup_listeners() { local listener_name="HTTPS-$domain" log "Starting cleanup for listener '$listener_name'..." + # Validate domain format + if ! validate_domain "$domain"; then + log_error "Invalid domain format: '$domain'" + return 1 + fi + + # Create backup directory with proper permissions + sudo mkdir -p "$BACKUP_DIR" || { log_error "Failed to create backup directory '$BACKUP_DIR'"; return 1; } + sudo chown -R "$(whoami)":"$(id -gn)" "$BACKUP_DIR" + sudo chmod 755 "$BACKUP_DIR" + # First backup the config local backup_file="${BACKUP_DIR}/httpd_config.pre-removal-$(date +%Y%m%d%H%M%S).xml" cp "$CONF_FILE" "$backup_file" || { @@ -159,15 +267,43 @@ cleanup_listeners() { } log_verbose "Created backup at: $backup_file" - # Remove the entire listener element with all its children - sudo xmlstarlet ed -L \ - -d "//listenerList/listener[name='$listener_name']" \ - "$CONF_FILE" || { - log_error "Failed to remove listener '$listener_name'" - cp "$backup_file" "$CONF_FILE" - return 1 - } - log_verbose "Removed listener element: $listener_name" + # Verify listener exists before attempting removal + if ! awk ' + // { in_listener=1; buffer="" } + in_listener { buffer = buffer $0 ORS } + /<\/listener>/ { + if (in_listener && buffer ~ /'"$domain"'/) { + print buffer + } + in_listener=0 + }' "$CONF_FILE" >/dev/null 2>&1; then + log "Listener for '$domain' not found in configuration. Skipping removal." + rm -f "$backup_file" + return 0 + fi + + # Remove the listener block using xmlstarlet + if ! sudo xmlstarlet ed --inplace -d "//listener[contains(., '$domain')]" "$CONF_FILE"; then + log_error "Failed to remove listener for '$domain'" + cp "$backup_file" "$CONF_FILE" + return 1 + fi + log_verbose "Removed listener block for domain: $domain" + + # Verify the listener was actually removed + if awk ' + // { in_listener=1; buffer="" } + in_listener { buffer = buffer $0 ORS } + /<\/listener>/ { + if (in_listener && buffer ~ /'"$domain"'/) { + print buffer + } + in_listener=0 + }' "$CONF_FILE" >/dev/null 2>&1; then + log_error "Listener for '$domain' still exists after removal attempt" + cp "$backup_file" "$CONF_FILE" + return 1 + fi # Validate XML after removal if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then @@ -177,36 +313,130 @@ cleanup_listeners() { fi log_verbose "XML validation passed after listener removal" - # Clean up any orphaned vhostMapList elements - sudo xmlstarlet ed -L \ - -d "//vhostMapList[not(parent::listener)]" \ - "$CONF_FILE" || { - log_error "Failed to clean up orphaned vhostMapList" - cp "$backup_file" "$CONF_FILE" - return 1 - } - log_verbose "Cleaned up orphaned vhostMapList elements" + # Set proper permissions on the config file + sudo chown litespeed:litespeed "$CONF_FILE" + sudo chmod 644 "$CONF_FILE" - # 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" + log_success "Successfully removed listener for '$domain'" + rm -f "$backup_file" + return 0 +} + +verify_cleanup_state() { + local domain="$1" + local listener_name="HTTPS-$domain" + + # Check certificate state + local cert_exists=0 + if certbot certificates | grep -q "Domains: $domain"; then + cert_exists=1 + fi + + # Check listener state - improved check + local listener_exists=0 + if xmlstarlet sel -t -c "//listenerList/listener[name='$listener_name']" "$CONF_FILE" >/dev/null 2>&1; then + listener_exists=1 + fi + + # Log current state + log_verbose "Current state for domain '$domain':" + log_verbose "Certificate exists: $cert_exists" + log_verbose "Listener exists: $listener_exists" + + # Return state code + echo "$cert_exists$listener_exists" +} + +validate_xml_structure() { + local file="$1" + + # Check for basic XML structure + if ! xmllint --noout "$file" 2>/dev/null; then + log_error "Invalid XML structure in $file" return 1 fi - log_verbose "XML validation passed after cleanup" + + # Check for required root elements + if ! xmlstarlet sel -t -v "//httpServerConfig" "$file" >/dev/null 2>&1; then + log_error "Missing httpServerConfig root element in $file" + return 1 + fi + + # Check for required sections + local required_sections=("listenerList" "virtualHostList") + for section in "${required_sections[@]}"; do + if ! xmlstarlet sel -t -v "//$section" "$file" >/dev/null 2>&1; then + log_error "Missing required section '$section' in $file" + return 1 + fi + done + + # Check for orphaned elements + if xmlstarlet sel -t -v "//vhostMapList[not(parent::listener)]" "$file" >/dev/null 2>&1; then + log_error "Found orphaned vhostMapList elements in $file" + return 1 + fi + + return 0 +} - log_success "Successfully removed listener '$listener_name' and cleaned up XML structure" - rm -f "$backup_file" +cleanup_xml() { + local file="$1" + local temp_file="${file}.tmp" + + # Create temporary copy + cp "$file" "$temp_file" || { + log_error "Failed to create temporary file for XML cleanup" + return 1 + } + + # Remove orphaned elements + if ! xmlstarlet ed -L \ + -d "//vhostMapList[not(parent::listener)]" \ + -d "//listener[not(.//vhostMapList)]" \ + "$temp_file"; then + log_error "Failed to clean up orphaned elements" + rm -f "$temp_file" + return 1 + fi + + # Validate cleaned XML + if ! validate_xml_structure "$temp_file"; then + log_error "Invalid XML structure after cleanup" + rm -f "$temp_file" + return 1 + fi + + # Move temporary file to actual config file + if ! mv "$temp_file" "$file"; then + log_error "Failed to update configuration file" + rm -f "$temp_file" + return 1 + fi + + # Set proper permissions + sudo chown litespeed:litespeed "$file" + sudo chmod 644 "$file" + return 0 } validate_xml() { log "Validating XML configuration..." - if ! sudo xmllint --noout "$CONF_FILE" 2>/dev/null; then - log "❌ ERROR: Invalid XML configuration after cleanup. Check backups." + + # First check with xmlstarlet + if ! sudo xmlstarlet val --well-formed "$CONF_FILE" >/dev/null 2>&1; then + log "❌ ERROR: XML configuration is not well-formed according to xmlstarlet. Check backups." return 1 fi - log "✔ XML configuration is valid." + + # Then check with xmllint for additional validation + if ! sudo xmllint --noout "$CONF_FILE" 2>/dev/null; then + log "❌ ERROR: XML configuration is invalid according to xmllint. Check backups." + return 1 + fi + + log "✔ XML configuration is valid (verified by both xmlstarlet and xmllint)." return 0 } @@ -229,6 +459,12 @@ main() { setup_logging log "Starting SSL Removal Process" + # Check and install required dependencies + log_verbose "Checking dependencies..." + check_command "xmlstarlet" "xmlstarlet" + check_command "xmllint" "libxml2-utils" + check_command "certbot" "certbot" + # Parse parameters while [[ $# -gt 0 ]]; do case "$1" in @@ -268,18 +504,51 @@ main() { continue fi - # Remove certificate first - if ! remove_certificate "$domain"; then - log "⚠ Warning: Failed to remove certificate for '$domain'. Continuing with cleanup..." - fi - - # Clean up listeners and configurations - if ! cleanup_listeners "$domain"; then - log_error "Failed to clean up listeners for '$domain'" - continue - fi + # Check current state + local state=$(verify_cleanup_state "$domain") + log_verbose "Current state code: $state" + + case "$state" in + "00") # No certificate, no listener + log "Domain '$domain' is already clean. No action needed." + continue + ;; + "01") # No certificate, listener exists + log "Found orphaned listener for '$domain'. Removing..." + if ! cleanup_listeners "$domain"; then + log_error "Failed to remove orphaned listener for '$domain'" + continue + fi + ;; + "10") # Certificate exists, no listener + log "Found orphaned certificate for '$domain'. Removing..." + if ! remove_certificate "$domain"; then + log_error "Failed to remove orphaned certificate for '$domain'" + continue + fi + ;; + "11") # Both certificate and listener exist + log "Found both certificate and listener for '$domain'. Removing both..." + if ! remove_certificate "$domain"; then + log "⚠ Warning: Failed to remove certificate for '$domain'. Continuing with listener cleanup..." + fi + if ! cleanup_listeners "$domain"; then + log_error "Failed to clean up listeners for '$domain'" + continue + fi + ;; + esac done + # Validate and clean up XML structure + if ! validate_xml_structure "$CONF_FILE"; then + log "Cleaning up XML structure..." + if ! cleanup_xml "$CONF_FILE"; then + log_error "Failed to clean up XML structure" + exit 1 + fi + fi + # Validate final XML configuration if validate_xml; then restart_litespeed diff --git a/scripts/ssl-manager/xmlchecker.sh b/scripts/ssl-manager/xmlchecker.sh index 1bb74ec..0613a9f 100644 --- a/scripts/ssl-manager/xmlchecker.sh +++ b/scripts/ssl-manager/xmlchecker.sh @@ -103,15 +103,28 @@ log_success() { check_command() { local cmd="$1" - local apt_pkg="$2" + local 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 + log "⚠ Required command '$cmd' not found. Attempting to install package '$pkg'..." + + # Try dnf first (AlmaLinux) + if command -v dnf &>/dev/null; then + if sudo dnf install -y "$pkg"; then + log "✔ Successfully installed '$pkg' using dnf." + return 0 + fi fi + + # Fallback to yum (CentOS) + if command -v yum &>/dev/null; then + if sudo yum install -y "$pkg"; then + log "✔ Successfully installed '$pkg' using yum." + return 0 + fi + fi + + log "❌ ERROR: Failed to install '$pkg' using either dnf or yum. Exiting." + exit 1 fi }