#!/bin/bash
# ProxMenux - Fail2Ban Installer & Configurator
# ============================================
# Author      : MacRimi
# License     : MIT
# Version     : 1.0
# ============================================
# Hybrid script: works from terminal (dialog) and web panel (ScriptTerminalModal)

SCRIPT_TITLE="Fail2Ban Installer for Proxmox VE"

LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
COMPONENTS_STATUS_FILE="$BASE_DIR/components_status.json"

export BASE_DIR
export COMPONENTS_STATUS_FILE

if [[ -f "$UTILS_FILE" ]]; then
  source "$UTILS_FILE"
fi

if [[ ! -f "$COMPONENTS_STATUS_FILE" ]]; then
  echo "{}" > "$COMPONENTS_STATUS_FILE"
fi

load_language
initialize_cache


# ==========================================================
# Detection
# ==========================================================
detect_fail2ban() {
  FAIL2BAN_INSTALLED=false
  FAIL2BAN_VERSION=""
  FAIL2BAN_ACTIVE=false

  if command -v fail2ban-client >/dev/null 2>&1; then
    FAIL2BAN_INSTALLED=true
    FAIL2BAN_VERSION=$(fail2ban-client --version 2>/dev/null | head -n1 | tr -d '[:space:]' || echo "unknown")

    if systemctl is-active --quiet fail2ban 2>/dev/null; then
      FAIL2BAN_ACTIVE=true
    fi
  fi
}


# ==========================================================
# Installation
# ==========================================================
install_fail2ban() {
  show_proxmenux_logo
  msg_title "$(translate "$SCRIPT_TITLE")"
  msg_info2 "$(translate "Installing and configuring Fail2Ban to protect Proxmox web interface and SSH...")"

  # Ensure Debian repositories are available
  local deb_codename
  deb_codename=$(grep -oP '^VERSION_CODENAME=\K.*' /etc/os-release 2>/dev/null)

  if ! grep -RqsE "debian.*(bookworm|trixie)" /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null; then
    msg_warn "$(translate "Debian repositories missing; creating default source file")"
    local src="/etc/apt/sources.list.d/debian.sources"
    cat > "$src" <<EOF
Types: deb
URIs: http://deb.debian.org/debian
Suites: ${deb_codename} ${deb_codename}-updates
Components: main contrib non-free non-free-firmware

Types: deb
URIs: http://security.debian.org/debian-security
Suites: ${deb_codename}-security
Components: main contrib non-free non-free-firmware
EOF
    msg_ok "$(translate "Debian repositories configured for ${deb_codename}")"
  fi

  # Install Fail2Ban
  msg_info "$(translate "Installing Fail2Ban...")"
  if ! DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null 2>&1 || \
     ! DEBIAN_FRONTEND=noninteractive apt-get install -y fail2ban >/dev/null 2>&1; then
    msg_error "$(translate "Failed to install Fail2Ban")"
    return 1
  fi
  msg_ok "$(translate "Fail2Ban installed successfully")"

  # ── Ensure journald stores auth-level messages ──
  # Proxmox sets MaxLevelStore=warning by default, which silently drops
  # info/notice messages. SSH auth failures (PAM) are logged at info/notice,
  # so Fail2Ban with backend=systemd will never see them.
  local journald_conf="/etc/systemd/journald.conf"
  local journald_changed=false

  if [[ -f "$journald_conf" ]]; then
    local current_max
    current_max=$(grep -i '^MaxLevelStore=' "$journald_conf" 2>/dev/null | tail -1 | cut -d= -f2 | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')

    # Levels that are too restrictive for auth logging (need at least info)
    case "$current_max" in
      emerg|alert|crit|err|warning)
        msg_warn "$(translate "journald MaxLevelStore is '${current_max}' - SSH auth failures are not being stored")"
        msg_info "$(translate "Updating journald to store info-level messages...")"

        # Create a drop-in so we don't break other Proxmox settings
        mkdir -p /etc/systemd/journald.conf.d
        cat > /etc/systemd/journald.conf.d/proxmenux-loglevel.conf <<'JEOF'
# ProxMenux: Allow auth/info messages so Fail2Ban can detect SSH failures
# Proxmox default MaxLevelStore=warning drops PAM/SSH auth events
[Journal]
MaxLevelStore=info
MaxLevelSyslog=info
JEOF
        journald_changed=true
        msg_ok "$(translate "journald drop-in created: /etc/systemd/journald.conf.d/proxmenux-loglevel.conf")"
        ;;
      *)
        msg_ok "$(translate "journald MaxLevelStore is adequate for auth logging")"
        ;;
    esac

    if $journald_changed; then
      systemctl restart systemd-journald
      sleep 1
      msg_ok "$(translate "journald restarted - auth messages will now be stored")"
    fi
  fi

  # ── Journal-to-file logger services for Fail2Ban ──
  # Fail2Ban's systemd backend has a known issue: it cannot reliably read
  # journal entries in real-time from certain services (pvedaemon workers,
  # and intermittently sshd). The solution is to create small systemd services
  # that tail the journal and write to log files, then fail2ban monitors those
  # files with the reliable backend=auto.

  # -- Proxmox UI auth logger (pvedaemon) --
  msg_info "$(translate "Creating Proxmox auth logger service...")"
  cat > /etc/systemd/system/proxmox-auth-logger.service <<'EOF'
[Unit]
Description=Proxmox Auth Logger for Fail2Ban
Documentation=https://github.com/MacRimi/ProxMenux
After=pvedaemon.service
PartOf=fail2ban.service

[Service]
Type=simple
ExecStart=/bin/bash -c 'journalctl -f _SYSTEMD_UNIT=pvedaemon.service -o short-iso --no-pager >> /var/log/proxmox-auth.log'
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

  touch /var/log/proxmox-auth.log
  chmod 640 /var/log/proxmox-auth.log
  chown root:adm /var/log/proxmox-auth.log 2>/dev/null || true

  systemctl daemon-reload
  systemctl enable --now proxmox-auth-logger.service >/dev/null 2>&1
  msg_ok "$(translate "Proxmox auth logger service created and started")"

  # -- SSH auth logger --
  msg_info "$(translate "Creating SSH auth logger service...")"
  cat > /etc/systemd/system/ssh-auth-logger.service <<'EOF'
[Unit]
Description=SSH Auth Logger for Fail2Ban
Documentation=https://github.com/MacRimi/ProxMenux
After=ssh.service
PartOf=fail2ban.service

[Service]
Type=simple
ExecStart=/bin/bash -c 'journalctl -f _SYSTEMD_UNIT=ssh.service -o short-iso --no-pager >> /var/log/ssh-auth.log'
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

  touch /var/log/ssh-auth.log
  chmod 640 /var/log/ssh-auth.log
  chown root:adm /var/log/ssh-auth.log 2>/dev/null || true

  systemctl daemon-reload
  systemctl enable --now ssh-auth-logger.service >/dev/null 2>&1
  msg_ok "$(translate "SSH auth logger service created and started")"

  # Configure Proxmox filter
  mkdir -p /etc/fail2ban/filter.d /etc/fail2ban/jail.d
  msg_info "$(translate "Configuring Proxmox filter...")"
  cat > /etc/fail2ban/filter.d/proxmox.conf <<'EOF'
[Definition]
# The proxmox-auth-logger service writes journal lines to /var/log/proxmox-auth.log
# in short-iso format: 2026-02-10T19:36:08+01:00 host pvedaemon[PID]: message
# Proxmox logs IPs as ::ffff:x.x.x.x (IPv4-mapped IPv6).
failregex = authentication (failure|error); rhost=(::ffff:)?<HOST> user=.* msg=.*
ignoreregex =
datepattern = ^%%Y-%%m-%%dT%%H:%%M:%%S
EOF
  msg_ok "$(translate "Proxmox filter configured")"

  # Configure Proxmox jail (file-based backend)
  msg_info "$(translate "Configuring Proxmox jail...")"
  cat > /etc/fail2ban/jail.d/proxmox.conf <<'EOF'
[proxmox]
enabled = true
port = 8006
filter = proxmox
backend = auto
logpath = /var/log/proxmox-auth.log
maxretry = 3
bantime = 3600
findtime = 600
EOF
  msg_ok "$(translate "Proxmox jail configured")"

  # Configure ProxMenux Monitor filter
  # This reads from a file written directly by the Flask app (not syslog/journal),
  # so it uses a datepattern that matches Python's logging format.
  msg_info "$(translate "Configuring ProxMenux Monitor filter...")"
  cat > /etc/fail2ban/filter.d/proxmenux.conf <<'EOF'
[Definition]
failregex = ^.*proxmenux-auth: authentication failure; rhost=<HOST> user=.*$
ignoreregex =
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S
EOF
  msg_ok "$(translate "ProxMenux Monitor filter configured")"

  # Configure ProxMenux Monitor jail (port 8008 + http/https for reverse proxy)
  # Uses backend=auto with logpath because the Flask app writes directly to this file.
  msg_info "$(translate "Configuring ProxMenux Monitor jail...")"
  cat > /etc/fail2ban/jail.d/proxmenux.conf <<'EOF'
[proxmenux]
enabled = true
port = 8008,http,https
filter = proxmenux
backend = auto
logpath = /var/log/proxmenux-auth.log
maxretry = 3
bantime = 3600
findtime = 600
EOF
  msg_ok "$(translate "ProxMenux Monitor jail configured")"

  # Ensure ProxMenux auth log exists (Flask writes here directly)
  touch /var/log/proxmenux-auth.log
  chmod 640 /var/log/proxmenux-auth.log 2>/dev/null || true

  # Detect firewall backend (nftables preferred, fallback to iptables)
  local ban_action="iptables-multiport"
  local ban_action_all="iptables-allports"
  if command -v nft >/dev/null 2>&1 && nft list ruleset >/dev/null 2>&1; then
    ban_action="nftables"
    ban_action_all="nftables[type=allports]"
    msg_ok "$(translate "Detected nftables - using nftables ban action")"
  else
    msg_info "$(translate "nftables not available - using iptables ban action")"
  fi

  # Configure global settings and SSH jail
  msg_info "$(translate "Configuring global Fail2Ban settings and SSH jail...")"
  cat > /etc/fail2ban/jail.local <<EOF
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1
ignoreself = true
bantime = 86400
maxretry = 2
findtime = 1800
backend = auto
banaction = ${ban_action}
banaction_allports = ${ban_action_all}

[sshd]
enabled = true
filter = sshd[mode=aggressive]
backend = auto
logpath = /var/log/ssh-auth.log
maxretry = 2
findtime = 3600
bantime = 32400
EOF
  msg_ok "$(translate "Global settings and SSH jail configured")"

  # ── SSH Hardening: MaxAuthTries ──
  # Lynis (SSH-7408) recommends MaxAuthTries=3. With fail2ban maxretry=2,
  # SSH will never reach 3 attempts, but setting it satisfies the audit
  # and adds defense-in-depth. Backup original value for clean restore.
  local sshd_config="/etc/ssh/sshd_config"
  if [[ -f "$sshd_config" ]]; then
    # Save original MaxAuthTries value for restore on uninstall
    local original_max_auth
    original_max_auth=$(grep -i '^MaxAuthTries' "$sshd_config" 2>/dev/null | awk '{print $2}' || echo "6")
    if [[ -z "$original_max_auth" ]]; then
      original_max_auth="6"
    fi

    # Store original value in our config directory
    echo "$original_max_auth" > "${BASE_DIR}/sshd_maxauthtries_backup"

    msg_info "$(translate "Hardening SSH: setting MaxAuthTries to 3...")"
    if grep -qi '^MaxAuthTries' "$sshd_config"; then
      sed -i 's/^MaxAuthTries.*/MaxAuthTries 3/' "$sshd_config"
    elif grep -qi '^#MaxAuthTries' "$sshd_config"; then
      sed -i 's/^#MaxAuthTries.*/MaxAuthTries 3/' "$sshd_config"
    else
      echo "MaxAuthTries 3" >> "$sshd_config"
    fi

    # Reload SSH to apply the change (reload, not restart, to keep existing sessions)
    systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
    msg_ok "$(translate "SSH MaxAuthTries set to 3 (original: ${original_max_auth})")"
  fi

  # Enable and restart the service (restart ensures new jails are loaded
  # even if fail2ban was already running from a previous install)
  systemctl daemon-reload
  systemctl enable fail2ban >/dev/null 2>&1
  systemctl restart fail2ban >/dev/null 2>&1
  sleep 3

  # Verify
  if systemctl is-active --quiet fail2ban; then
    msg_ok "$(translate "Fail2Ban is running correctly")"
  else
    msg_error "$(translate "Fail2Ban is NOT running!")"
    journalctl -u fail2ban --no-pager -n 10
  fi

  if fail2ban-client ping >/dev/null 2>&1; then
    msg_ok "$(translate "fail2ban-client successfully communicated with the server")"
  else
    msg_error "$(translate "fail2ban-client could not communicate with the server")"
  fi

  update_component_status "fail2ban" "installed" "$(fail2ban-client --version 2>/dev/null | head -n1)" "security" '{}'

  msg_success "$(translate "Fail2Ban installation and configuration completed successfully!")"
}


# ==========================================================
# Uninstall
# ==========================================================
uninstall_fail2ban() {
  show_proxmenux_logo
  msg_title "$(translate "$SCRIPT_TITLE")"
  msg_info2 "$(translate "Removing Fail2Ban...")"

  systemctl stop fail2ban 2>/dev/null || true
  systemctl disable fail2ban 2>/dev/null || true

  # Stop and remove the auth logger services
  systemctl stop proxmox-auth-logger.service 2>/dev/null || true
  systemctl disable proxmox-auth-logger.service 2>/dev/null || true
  rm -f /etc/systemd/system/proxmox-auth-logger.service
  systemctl stop ssh-auth-logger.service 2>/dev/null || true
  systemctl disable ssh-auth-logger.service 2>/dev/null || true
  rm -f /etc/systemd/system/ssh-auth-logger.service
  systemctl daemon-reload 2>/dev/null || true
  rm -f /var/log/proxmox-auth.log /var/log/ssh-auth.log
  
  DEBIAN_FRONTEND=noninteractive apt-get purge -y fail2ban >/dev/null 2>&1
  rm -f /etc/fail2ban/jail.d/proxmox.conf
  rm -f /etc/fail2ban/jail.d/proxmenux.conf
  rm -f /etc/fail2ban/filter.d/proxmox.conf
  rm -f /etc/fail2ban/filter.d/proxmenux.conf
  rm -f /etc/fail2ban/jail.local

  # ── Restore SSH MaxAuthTries to original value ──
  local sshd_config="/etc/ssh/sshd_config"
  local backup_file="${BASE_DIR}/sshd_maxauthtries_backup"
  if [[ -f "$backup_file" && -f "$sshd_config" ]]; then
    local original_val
    original_val=$(cat "$backup_file" 2>/dev/null | tr -d '[:space:]')
    if [[ -n "$original_val" ]]; then
      msg_info "$(translate "Restoring SSH MaxAuthTries to ${original_val}...")"
      if grep -qi '^MaxAuthTries' "$sshd_config"; then
        sed -i "s/^MaxAuthTries.*/MaxAuthTries ${original_val}/" "$sshd_config"
      fi
      systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
      msg_ok "$(translate "SSH MaxAuthTries restored to ${original_val}")"
    fi
    rm -f "$backup_file"
  fi

  # Remove journald drop-in and restore original log level
  if [[ -f /etc/systemd/journald.conf.d/proxmenux-loglevel.conf ]]; then
    rm -f /etc/systemd/journald.conf.d/proxmenux-loglevel.conf
    systemctl restart systemd-journald 2>/dev/null || true
    msg_ok "$(translate "journald log level restored")"
  fi

  update_component_status "fail2ban" "removed" "" "security" '{}'

  msg_ok "$(translate "Fail2Ban has been removed")"
  msg_success "$(translate "Uninstallation completed. Press Enter to continue...")"
  read -r
}


# ==========================================================
# Main
# ==========================================================
main() {
  detect_fail2ban

  if $FAIL2BAN_INSTALLED; then
    # Already installed - show action menu
    local action_text
    action_text="\n$(translate 'Fail2Ban is currently installed.')\n"
    action_text+="$(translate 'Version:') $FAIL2BAN_VERSION\n"
    action_text+="$(translate 'Status:') $(if $FAIL2BAN_ACTIVE; then translate 'Active'; else translate 'Inactive'; fi)\n\n"
    action_text+="$(translate 'What would you like to do?')"

    local ACTION
    ACTION=$(hybrid_menu "$(translate 'Fail2Ban Management')" "$action_text" 18 70 3 \
      "reinstall" "$(translate 'Reinstall and reconfigure Fail2Ban')" \
      "remove" "$(translate 'Uninstall Fail2Ban')" \
      "cancel" "$(translate 'Cancel')" \
    ) || ACTION="cancel"

    case "$ACTION" in
      reinstall)
        if hybrid_yesno "$(translate 'Reinstall Fail2Ban')" \
          "\n\n$(translate 'This will reinstall and reconfigure Fail2Ban with the default ProxMenux settings. Continue?')" 12 70; then
          install_fail2ban
        fi
        ;;
      remove)
        if hybrid_yesno "$(translate 'Remove Fail2Ban')" \
          "\n\n$(translate 'This will completely remove Fail2Ban and its configuration. Continue?')" 12 70; then
          uninstall_fail2ban
        fi
        ;;
      cancel|*)
        exit 0
        ;;
    esac
  else
    # Not installed - confirm and install
    local info_text
    info_text="\n$(translate 'Fail2Ban is not installed on this system.')\n\n"
    info_text+="$(translate 'This will install and configure Fail2Ban with:')\n\n"
    info_text+="  - $(translate 'SSH protection (aggressive mode)') (max 2 $(translate 'retries'), 9h $(translate 'ban'))\n"
    info_text+="  - $(translate 'Proxmox web interface protection') ($(translate 'port') 8006, max 3 $(translate 'retries'), 1h $(translate 'ban'))\n"
    info_text+="  - $(translate 'ProxMenux Monitor protection') ($(translate 'port') 8008 + $(translate 'reverse proxy'), max 3 $(translate 'retries'), 1h $(translate 'ban'))\n"
    info_text+="  - $(translate 'Auto-detected firewall backend (nftables/iptables)')\n"
    info_text+="  - $(translate 'Adjusts journald log level if needed (Proxmox defaults may block auth logs)')\n"
    info_text+="  - $(translate 'SSH hardening: MaxAuthTries set to 3 (Lynis recommendation)')\n\n"
    info_text+="$(translate 'Do you want to proceed?')"

    if hybrid_yesno "$(translate 'Install Fail2Ban')" "$info_text" 20 70; then
      install_fail2ban
    else
      exit 0
    fi
  fi
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main
fi
