diff --git a/mbadmin.jps b/mbadmin.jps index 37e64a2..163da83 100644 --- a/mbadmin.jps +++ b/mbadmin.jps @@ -34,6 +34,8 @@ onInstall: - 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/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 # Install Certbot for AlmaLinux - dnf install -y certbot @@ -143,12 +145,6 @@ menu: action: remove_ssl_cert settings: sslRemoveConfig 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? loadingText: Analyzing configuration... caption: Diagnose LiteSpeed Config @@ -589,10 +585,10 @@ actions: - cmd[cp]: user: root 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: type: info - message: "SSL certificate for '${settings.domain}' has been issued successfully." + message: "SSL certificate issuance process completed." check_domain_ip: - cmd[cp]: user: root @@ -605,82 +601,15 @@ actions: - cmd[cp]: user: root 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: type: info - message: "${response.out}" + message: "SSL certificate removal process completed." diagnose_litespeed_config: - cmd[cp]: user: root commands: - - | - CONF_FILE="/var/www/conf/httpd_config.xml" - echo "Analyzing LiteSpeed configuration tags..." - echo "-----------------------------------" - num_n=$(grep -c '' "${CONF_FILE}") - num_n_close=$(grep -c '' "${CONF_FILE}") - num_name=$(grep -c '' "${CONF_FILE}") - num_name_close=$(grep -c '' "${CONF_FILE}") - echo "Number of tags: $num_n" - echo "Number of tags: $num_n_close" - echo "Number of tags: $num_name" - echo "Number of tags: $num_name_close" - echo "-----------------------------------" - echo "First 5 instances of tags:" - grep -n '' "${CONF_FILE}" | head -5 - echo "-----------------------------------" - echo "Testing sed command effectiveness:" - cp "${CONF_FILE}" /tmp/test_config.xml - sed -i 's###g' /tmp/test_config.xml - sed -i 's###g' /tmp/test_config.xml - new_num_n=$(grep -c '' /tmp/test_config.xml) - new_num_n_close=$(grep -c '' /tmp/test_config.xml) - echo "After sed, remaining tags: $new_num_n" - echo "After sed, remaining 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; } - // { in_listener = 1; print; next; } - in_listener && (/HTTPS<\/name>/ || /HTTPS-ipv6<\/name>/) { - is_shared = 1; print; next; - } - in_listener && is_shared && /.*live\/'"${DOMAIN}"'\/.*<\/keyFile>/ { - print " /var/www/conf/default.key"; next; - } - in_listener && is_shared && /.*live\/'"${DOMAIN}"'\/.*<\/certFile>/ { - print " /var/www/conf/default.crt"; next; - } - /<\/listener>/ { in_listener = 0; is_shared = 0; print; next; } - { print; } - ' "${CONF_FILE}" > "${TEMP_FILE}" - - if grep -q "" "${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 + - bash /home/litespeed/mbmanager/ssl-manager/xmlchecker.sh --verbose - return: type: info message: "${response.out}" diff --git a/scripts/ssl-manager/ssl_manager.sh b/scripts/ssl-manager/ssl_manager.sh index 60ebaf7..f727937 100644 --- a/scripts/ssl-manager/ssl_manager.sh +++ b/scripts/ssl-manager/ssl_manager.sh @@ -3,285 +3,465 @@ # 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) +# Version: 2.0.4 (Idempotent, Clean Exit Messages) # 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 +PUBLIC_IP="" +DOMAINS=() +EMAIL="" 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" +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 === -trap 'log "Script interrupted. Cleaning up temporary files..."; sudo rm -f "$BACKUP_FILE"' EXIT +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 +} -# === Functions === 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() { - [[ "$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() { 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'..." + 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'." + log_success "Successfully installed '$apt_pkg'" else - log "❌ ERROR: Failed to install '$apt_pkg'. Exiting." - exit 1 + log_error "Failed to install '$apt_pkg'" + SCRIPT_EXIT_STATUS=1; return 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 -} +validate_domain() { [[ "$1" =~ ^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$ ]]; } +validate_ip() { [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; } +validate_email() { [[ "$1" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; } 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 + sudo cp -a "$CONF_FILE" "$DEFAULT_CONF" || { + log_error "FATAL: Failed to create initial backup '$DEFAULT_CONF'" + SCRIPT_EXIT_STATUS=2; return 1 + } + log_success "Created initial backup successfully" else - log "Info: Default backup '$DEFAULT_CONF' already exists. Skipping creation." + log "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." + log_verbose "Validating XML structure of '$1'..." + sudo xmllint --noout "$1" 2>/dev/null || { + log_error "Invalid XML structure in '$1'" + SCRIPT_EXIT_STATUS=1 return 1 - fi + } + log_verbose "XML validation passed for '$1'" } 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 + resolved_ip=$(dig +short "$1" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1) + [[ "$resolved_ip" == "$2" ]] } 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 + [[ "$(curl -s "http://$1/.well-known/acme-challenge/test-token")" == "$token" ]] } 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 + if [[ -f "$CERT_DIR/$1/fullchain.pem" ]]; then + log_success "Certificate already exists for '$1'. Skipping issuance." + return 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() { local domain="$1" 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 + + + + $vhost_name + $domain + + +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'..." + sudo cp -a "$CONF_FILE" "$BACKUP_FILE" - # 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 + # Create a temporary XML file with the new listener configuration + local temp_xml=$(mktemp) + cat > "$temp_xml" << EOF + + + $listener_name +
*:443
+ 1 + /etc/letsencrypt/live/$domain/privkey.pem + /etc/letsencrypt/live/$domain/fullchain.pem + + + $vhost_name + $domain + + +
+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 - # Update the listener configuration using xmlstarlet - log "Adding listener for domain '$domain' with IP '$ip'..." + # Insert the new listener into the configuration using a different approach 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" + -s "//listenerList" \ + -t elem -n "listener" \ + "$CONF_FILE" || { + rm -f "$temp_xml" + log_error "Failed to create new listener element" + SCRIPT_EXIT_STATUS=1 + return 1 + } - # Validate the updated configuration - if validate_xml "$CONF_FILE"; then - log "✔ Updated configuration is valid." - else - log "❌ ERROR: Updated configuration is invalid. Restoring backup..." + # Now add each element individually + sudo xmlstarlet ed -L \ + -s "//listenerList/listener[last()]" \ + -t elem -n "name" \ + -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" - log "✔ Backup restored: '$CONF_FILE'." - exit 1 + SCRIPT_EXIT_STATUS=1 + return 1 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() { 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 systemctl restart lsws || { + log_error "Failed to restart LiteSpeed. Restoring backup..." sudo cp -a "$BACKUP_FILE" "$CONF_FILE" - log "✔ Backup restored: '$CONF_FILE'." - exit 3 - fi + SCRIPT_EXIT_STATUS=3 + return 1 + } + log_success "LiteSpeed server restarted successfully" } # === 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 -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)." + # Parse parameters + for arg in "$@"; do + case $arg in + --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 -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 + [[ -z "$PRIMARY_DOMAIN" || -z "$PUBLIC_IP" || -z "$EMAIL" ]] && { + log_error "Missing required parameters. Provide --domains, --public-ip, and --email." + SCRIPT_EXIT_STATUS=1; exit 1 + } -# 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 + validate_domain "$PRIMARY_DOMAIN" || { log_error "Invalid domain '$PRIMARY_DOMAIN'"; SCRIPT_EXIT_STATUS=1; exit 1; } + validate_ip "$PUBLIC_IP" || { log_error "Invalid IP '$PUBLIC_IP'"; SCRIPT_EXIT_STATUS=1; exit 1; } + validate_email "$EMAIL" || { log_error "Invalid email '$EMAIL'"; SCRIPT_EXIT_STATUS=1; exit 1; } -if ! validate_domain "$PRIMARY_DOMAIN"; then - log "❌ ERROR: Invalid domain '$PRIMARY_DOMAIN'." - exit 1 -fi + validate_dns "$PRIMARY_DOMAIN" "$PUBLIC_IP" || { log_error "DNS validation failed"; SCRIPT_EXIT_STATUS=1; exit 1; } + validate_http_access "$PRIMARY_DOMAIN" || { log_error "HTTP access validation failed"; SCRIPT_EXIT_STATUS=1; exit 1; } -if ! validate_ip "$PUBLIC_IP"; then - log "❌ ERROR: Invalid IP address '$PUBLIC_IP'." - exit 1 -fi + log_verbose "Checking dependencies..." + check_command xmllint libxml2-utils + check_command xmlstarlet xmlstarlet + check_command certbot certbot -if ! validate_email "$EMAIL"; then - log "❌ ERROR: Invalid email address '$EMAIL'." - exit 1 -fi + create_default_backup + issue_certificate "$PRIMARY_DOMAIN" "$EMAIL" -# Validate DNS and HTTP Access -if ! validate_dns "$PRIMARY_DOMAIN" "$PUBLIC_IP"; then - log "❌ ERROR: DNS validation failed. Exiting." - exit 1 -fi + for domain in "${DOMAINS[@]}"; do + update_httpd_config "$domain" "$PUBLIC_IP" + cleanup_xml "$domain" + done -if ! validate_http_access "$PRIMARY_DOMAIN"; then - log "❌ ERROR: HTTP access validation failed. Exiting." - exit 1 -fi + restart_litespeed -# Dependency Checks -log_verbose "Checking dependencies..." -check_command "xmllint" "libxml2-utils" -check_command "xmlstarlet" "xmlstarlet" -check_command "certbot" "certbot" + log_success "SSL Manager completed successfully" + SCRIPT_EXIT_STATUS=0 +} -# 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 \ No newline at end of file +# === Entry Point === +main "$@" \ No newline at end of file diff --git a/scripts/ssl-manager/ssl_remover.sh b/scripts/ssl-manager/ssl_remover.sh index 70777cd..4569a07 100644 --- a/scripts/ssl-manager/ssl_remover.sh +++ b/scripts/ssl-manager/ssl_remover.sh @@ -20,15 +20,65 @@ BACKUP_DIR="/var/www/conf/backups" LOG_DIR="/var/log/mb-ssl" CERT_DIR="/etc/letsencrypt/live" 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 # === 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() { - 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() { - [[ "$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() { @@ -80,12 +130,15 @@ remove_certificate() { log "Checking for certificate for domain '$domain'..." 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 + # Remove all certificate files 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 - log "❌ ERROR: Failed to remove certificate for '$domain'." + log_error "Failed to remove certificate for '$domain'" return 1 fi else @@ -95,28 +148,56 @@ remove_certificate() { cleanup_listeners() { 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 - sudo xmlstarlet ed -L \ - -d "//listener[name='HTTPS-$domain']" \ - "$CONF_FILE" + # 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" || { + 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 \ - -d "//vhostMap[domain='$domain']" \ - "$CONF_FILE" + -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" - # Remove related virtual host - local vhost_name="${domain//./_}" - sudo xmlstarlet ed -L \ - -d "//virtualHost[name='$vhost_name']" \ - "$CONF_FILE" + # Validate XML after removal + if ! xmllint --noout "$CONF_FILE" 2>/dev/null; then + log_error "Invalid XML structure after listener removal. Restoring backup..." + cp "$backup_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 \ - -d "//listenerList[count(*)=0]" \ - "$CONF_FILE" + -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" + + # 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() { @@ -144,21 +225,27 @@ main() { declare -a DOMAINS EMAIL="" + # Setup logging first + setup_logging + log "Starting SSL Removal Process" + # Parse parameters while [[ $# -gt 0 ]]; do case "$1" in --domains=*) IFS=',' read -ra DOMAINS <<< "${1#*=}" + log_verbose "Parsed domains: ${DOMAINS[*]}" ;; --email=*) EMAIL="${1#*=}" + log_verbose "Set email notification to: $EMAIL" ;; --verbose) VERBOSE=1 - log "Verbose mode enabled." + log "Verbose mode enabled" ;; *) - log "Invalid parameter: $1" + log_error "Invalid parameter: $1" exit 1 ;; esac @@ -167,40 +254,39 @@ main() { # Validate input if [[ ${#DOMAINS[@]} -eq 0 ]]; then - log "❌ ERROR: --domains parameter is required." + log_error "--domains parameter is required" exit 1 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 for domain in "${DOMAINS[@]}"; do log "Processing domain: $domain" # Validate domain format if ! validate_domain "$domain"; then - log "❌ ERROR: Invalid domain '$domain'. Skipping." + log_error "Invalid domain '$domain'. Skipping." continue fi - # Remove certificate - remove_certificate "$domain" + # 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 - cleanup_listeners "$domain" + if ! cleanup_listeners "$domain"; then + log_error "Failed to clean up listeners for '$domain'" + continue + fi done - # Validate XML configuration + # Validate final XML configuration if validate_xml; then restart_litespeed + log_success "SSL Removal completed successfully for domains: ${DOMAINS[*]}" send_email "SSL Removal Complete" "Successfully removed SSL for domains: ${DOMAINS[*]}" 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[*]}" exit 1 fi diff --git a/scripts/ssl-manager/xmlchecker.sh b/scripts/ssl-manager/xmlchecker.sh index dbc41f4..1bb74ec 100644 --- a/scripts/ssl-manager/xmlchecker.sh +++ b/scripts/ssl-manager/xmlchecker.sh @@ -22,13 +22,19 @@ SERVER_ROOT="/var/www" # Server root directory LOG_DIR="/var/log/mb-ssl" # Log directory 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 - 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 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 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 @@ -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 # === 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() { - 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() { - [[ "$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() { @@ -61,6 +115,70 @@ check_command() { 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() { local file_to_check="$1" log_verbose "Performing semantic checks on '$file_to_check'..." @@ -68,41 +186,25 @@ perform_semantic_checks() { # Check for critical elements if ! sudo xmlstarlet sel -t -c "//virtualHostList" "$file_to_check" &>/dev/null; then - log "Warning: is missing in '$file_to_check'." + log_error "Missing in '$file_to_check'" semantic_errors=$((semantic_errors + 1)) fi if ! sudo xmlstarlet sel -t -c "//listenerList" "$file_to_check" &>/dev/null; then - log "Warning: is missing in '$file_to_check'." + log_error "Missing in '$file_to_check'" semantic_errors=$((semantic_errors + 1)) fi # Validate listener certificates - local listeners - listeners=$(sudo xmlstarlet sel -Q -t -v "//listenerList/listener/name" "$file_to_check" 2>/dev/null || echo "") - for listener in $listeners; do - 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 + if ! validate_listener_certificates "$file_to_check"; then + semantic_errors=$((semantic_errors + 1)) + fi # Return result 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 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 fi } @@ -189,84 +291,88 @@ apply_and_restart() { } # === Main Script Logic === -log "Starting XML config check/fix V1.8.0 for $CONF_FILE" -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)." +main() { + # Setup logging first + setup_logging + log "Starting XML config check/fix V1.8.0 for $CONF_FILE" -# Argument Parsing -if [[ "${1:-}" == "--verbose" ]]; then - VERBOSE=1 - log "Verbose mode enabled." -fi + # Argument Parsing + if [[ "${1:-}" == "--verbose" ]]; then + VERBOSE=1 + log "Verbose mode enabled" + fi -# Dependency Checks -log_verbose "Checking dependencies..." -check_command "xmllint" "libxml2-utils" -check_command "xmlstarlet" "xmlstarlet" + # Dependency Checks + log_verbose "Checking dependencies..." + check_command "xmllint" "libxml2-utils" + check_command "xmlstarlet" "xmlstarlet" -# Backup Original File -log "Backing up '$CONF_FILE' to '$BACKUP_FILE'..." -sudo cp -a "$CONF_FILE" "$BACKUP_FILE" + # Backup Original File + log "Backing up '$CONF_FILE' to '$BACKUP_FILE'..." + sudo cp -a "$CONF_FILE" "$BACKUP_FILE" -# Initial Validation -log_verbose "Running initial xmllint validation on '$CONF_FILE'..." -if sudo xmllint --noout "$CONF_FILE" 2>"$XML_ERROR_LOG"; then - log "✔ Initial Check: '$CONF_FILE' is valid XML." - INITIAL_FILE_VALID=1 -else - log "⚠ Initial Check: '$CONF_FILE' is invalid XML. Will attempt recovery/cleanup." -fi + # Initial Validation + log_verbose "Running initial xmllint validation on '$CONF_FILE'..." + if sudo xmllint --noout "$CONF_FILE" 2>/dev/null; then + log_success "Initial Check: '$CONF_FILE' is valid XML" + INITIAL_FILE_VALID=1 + else + log_error "Initial Check: '$CONF_FILE' is invalid XML. Will attempt recovery/cleanup" + fi -# Recovery Attempt -if [[ "$INITIAL_FILE_VALID" -eq 0 ]]; then - log "Attempting automatic recovery with 'xmllint --recover'..." - sudo rm -f "$RECOVERY_TMP_FILE" - if xmllint --recover "$CONF_FILE" 2>/dev/null > "$RECOVERY_TMP_FILE"; then - if sudo xmllint --noout "$RECOVERY_TMP_FILE" 2>/dev/null; then - log "✔ 'xmllint --recover' produced structurally valid XML output." - RECOVERY_PRODUCED_VALID_XML=1 + # Recovery Attempt + if [[ "$INITIAL_FILE_VALID" -eq 0 ]]; then + log "Attempting automatic recovery with 'xmllint --recover'..." + sudo rm -f "$RECOVERY_TMP_FILE" + if xmllint --recover "$CONF_FILE" 2>/dev/null > "$RECOVERY_TMP_FILE"; then + if sudo xmllint --noout "$RECOVERY_TMP_FILE" 2>/dev/null; then + log_success "'xmllint --recover' produced structurally valid XML output" + 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 - 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" fi - else - log "⚠ 'xmllint --recover' command failed or produced errors. Discarding recovery." - sudo rm -f "$RECOVERY_TMP_FILE" fi -fi -# Final Validation and Decision -FINAL_CONFIG_SOURCE_FILE="" -FINAL_CONFIG_SOURCE_DESC="" -FINAL_EXIT_CODE=0 -log_verbose "Running final validation and decision logic..." + # Final Validation and Decision + FINAL_CONFIG_SOURCE_FILE="" + FINAL_CONFIG_SOURCE_DESC="" + FINAL_EXIT_CODE=0 + log_verbose "Running final validation and decision logic..." -# Check 1: Is the original (potentially cleaned) temp file valid now? -if [[ "$INITIAL_FILE_VALID" -eq 1 ]]; then - FINAL_CONFIG_SOURCE_FILE="$CONF_FILE" - FINAL_CONFIG_SOURCE_DESC="original" -elif [[ "$RECOVERY_PRODUCED_VALID_XML" -eq 1 ]]; then - FINAL_CONFIG_SOURCE_FILE="$RECOVERY_TMP_FILE" - 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" + # Check 1: Is the original (potentially cleaned) temp file valid now? + if [[ "$INITIAL_FILE_VALID" -eq 1 ]]; then + FINAL_CONFIG_SOURCE_FILE="$CONF_FILE" + FINAL_CONFIG_SOURCE_DESC="original" + elif [[ "$RECOVERY_PRODUCED_VALID_XML" -eq 1 ]]; then + FINAL_CONFIG_SOURCE_FILE="$RECOVERY_TMP_FILE" + FINAL_CONFIG_SOURCE_DESC="recovered" 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 fi -else - log "❌ FATAL: No valid configuration file found. Exiting." - exit 1 -fi +} + +# === Entry Point === +main "$@" # === Fallback Logic === log "Attempting fallback sequence..."