#!/bin/bash

LOGFILE="/var/log/ban-abusers.log"

# ------------------------------------------------------------
# Configuration
# ------------------------------------------------------------

# Normal HTTP 4xx threshold
limit=30

# Only scan access-log files modified within this many minutes
mmin="-20"

# XWiki recursive redirect threshold
xwiki_register_limit=5

# Maximum number of recursive URL-decoding passes
xwiki_decode_passes=10

# ------------------------------------------------------------
# Data structures & Regexes
# ------------------------------------------------------------

declare -A recidive_ignored
declare -A recidive_banned

ip_regex='([0-9]{1,3}\.){3}[0-9]{1,3}|([0-9A-Fa-f]{1,4}:){2,}[0-9A-Fa-f:.]*'
http_4xx_regex='" 4[0-9]{2} [0-9]+ "'
ignored_images_regex='\.(png|jpe?g|gif|webp|svg|ico|bmp|tiff?|avif)(\?.*)? HTTP/'

# ------------------------------------------------------------
# Helper functions
# ------------------------------------------------------------

# Load IPs from fail2ban into an associative array
load_ips() {
    local -n map=$1
    while read -r ip; do
        [[ -n "$ip" ]] && map["$ip"]=1
    done < <(fail2ban-client get recidive "$2" 2>/dev/null | grep -oE "$ip_regex")
}

# Ban an IP address if not ignored or already banned
ban_ip() {
    local ip="$1"
    local reason="$2"

    [[ -z "$ip" ]] && return

    # Ignore explicitly trusted addresses
    if [[ -n "${recidive_ignored[$ip]}" ]]; then
        echo "Ignoring $ip (in ignoreip)"
        return
    fi

    # Do not ban an address already banned by recidive
    if [[ -n "${recidive_banned[$ip]}" ]]; then
        return
    fi

    echo "Banning $ip ($reason)"

    if fail2ban-client set recidive banip "$ip" >/dev/null 2>&1; then
        echo "$(date '+%Y-%m-%d %H:%M:%S') Banned $ip: $reason" >> "$LOGFILE"
        recidive_banned["$ip"]=1
    fi
}

# ------------------------------------------------------------
# Initialize Fail2Ban state
# ------------------------------------------------------------

load_ips recidive_ignored ignoreip
load_ips recidive_banned banip

# ------------------------------------------------------------
# Find Apache access logs
# ------------------------------------------------------------

find_args=(
    /var/log/apache2
    -type f
    -name "*access.log"
    ! -name "test.odo.lv_access.log"
    ! -name "localhost_access.log"
    -mmin "$mmin"
)

# ------------------------------------------------------------
# Normal HTTP 4xx abuse detection
# ------------------------------------------------------------

bad_ips=$(
    find "${find_args[@]}" -exec cat {} + 2>/dev/null |
        grep -E "$http_4xx_regex" |
        grep -E -i -v "$ignored_images_regex" |
        awk -v limit="$limit" '{count[$1]++} END {for (ip in count) if (count[ip] > limit) print ip}'
)

while read -r ip; do
    ban_ip "$ip" "more than $limit HTTP 4xx requests"
done <<< "$bad_ips"

# ------------------------------------------------------------
# XWiki recursive redirect detection
# ------------------------------------------------------------

bad_xwiki_ips=$(
    XWIKI_REGISTER_LIMIT="$xwiki_register_limit" \
    XWIKI_DECODE_PASSES="$xwiki_decode_passes" \
    find "${find_args[@]}" -exec perl -ne '
        my ($ip, $request) = /^(\S+)\s+.*?"\S+\s+(\S+)\s+HTTP\/[^"]+/;
        next unless defined $ip && defined $request;
        next unless $request =~ /xredirect=/i && $request =~ m{/xwiki/bin/(?:login|register)/XWiki/XWiki(?:Login|Register)}i;

        my $decoded = $request;
        for (1 .. ($ENV{XWIKI_DECODE_PASSES} || 10)) {
            last unless $decoded =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/ge;
        }

        my $count = () = $decoded =~ m{/xwiki/bin/register/XWiki/XWikiRegister}g;
        print "$ip|$count\n" if $count >= ($ENV{XWIKI_REGISTER_LIMIT} || 3);
    ' {} + 2>/dev/null |
    sort -t'|' -k1,1 -k2,2nr |
    awk -F'|' '!seen[$1]++ {print}'
)

while IFS='|' read -r ip count; do
    ban_ip "$ip" "XWiki recursive redirect: $count registration URLs"
done <<< "$bad_xwiki_ips"

echo "Banning Complete"

