#!/bin/bash
# anti-flood script
# Example usage: ./log_flood_block.sh /home/portalwakatobika/access-logs/portal.wakatobikab.go.id-ssl_log
# the script will:
# - filter out any CloudFlare owned IPs and any IPs already present in /etc/csf/csf.deny
# - anything else from that log file will be added to /etc/csf/csf.deny
# - the access-log logfile will be truncated afterwards so the script don't have to process the same IPs again
# - all IPs listed by LiteSpeed (in the /tmp/lshttpd/.rtreport* files under the BLOCKED_IP section) will be blocked,
#   excluding CloudFlare IPs and IPs that are already blocked
# - it runs in a loop with 30 seconds pause per round until stopped
#
# 19-11-2023 lukasz@worldhost.group
# 07-03-2025 added auto exit when the flood is over + removal of blocked ips within 24h
# 25-04-2025 cleanup unused function, update selected fragments flagged by shellcheck, added domain check:
#            refuse to run if domain is behind cloudflare (provide .htaccess 500 hint);
#            added never_block list of IPs
##

bin_grepcidr=$(which grepcidr)
if [[ ! -x ${bin_grepcidr} ]]; then
        echo "grepcidr missing, try: yum install grepcidr (epel)"
        exit 1
fi

# parse httpd log file, ban any non-cloudflare IPs
function log_parse_and_ban() {
        csf_blocked=$(grep "^[1-9]" /etc/csf/csf.deny|awk '{print $1}'|grep ':' -v)
        for cur_logline in $(awk '{print $1}' "${log_file}" |sort|uniq|grep -Ev '(:|127.0.0)'); do
#               echo "processing ${cur_logline}"
                is_cf=$(${bin_grepcidr} 1>/dev/null -f /root/cf_ips.txt <<<"${cur_logline}";echo $?)
                is_never_block=$(grep -qw "${cur_logline}" <<<"${never_block}";echo $?)
                if [[ ${is_cf} -eq 0 ]]; then
                        echo "${cur_logline} skipped, CloudFlare IP"
                  elif [[ ${is_never_block} -eq 0 ]]; then
                        echo "${cur_logline} skipped, IP on never_block list"
                  else
#                       echo -n "checking if ${cur_logline} is already in csf.deny: "
                        is_csf_blocked=$(grep -Eq "^${cur_logline}$" <<<"${csf_blocked}";echo $?)
                        if [[ ${is_csf_blocked} -eq 0 ]]; then
                                echo "${cur_logline} already blocked, skipped"
                          else
                                new_block=1
                                echo "${cur_logline} # log_flood_block.sh: $(date)" >>/etc/csf/csf.deny
                                echo "${cur_logline} blocked"
                        fi
                fi
        done
        # flush log file to avoid processing the same IPs
        :>"${log_file}"
}

usage() {
        echo -e "\nusage: $0 <log_file> <domain>"
        echo "example: $0 /home/malakak2/access-logs/layanan.malakakab.go.id-ssl_log layanan.malakakab.go.id"
        exit 0
}

##main
# get cloudflare ips
log_file=$1
domain=$2

# never block IPs:
never_block="77.95.113.232 65.181.111.253"

# check CSF
if [[ ! -d /etc/csf ]]; then
        echo "CSF not found, exiting."
        exit 1
fi

# startup checks
if [[ -z ${log_file} ]]; then usage; fi
if [[ ! -f ${log_file} ]]; then echo "\"${log_file}\": no such file"; usage; fi
if [[ -z ${domain} ]]; then usage; fi

# get cloudflare ranges
curl -s https://www.cloudflare.com/ips-v4 >/root/cf_ips.txt

# make sure the domain is not behind cloudflare
dm_ip=$(dig -t a "${domain}" +short|head -1)
is_dm_cf=$(${bin_grepcidr} 1>/dev/null -f /root/cf_ips.txt <<<"${dm_ip}";echo $?)
if [[ ${is_dm_cf} -eq 0 ]]; then
        echo "ERROR: domain ${domain} is behind CloudFlare - blocking IPs from the log would not block the flood, instead please add rewrite rule on top of relevant .htaccess file with the following content:"
        echo -e "\nRewriteEngine On\nRewriteRule .* - [R=500,L]\n"
        echo "exiting."
        exit 0
fi

# report current DENY_IP_LIMIT value
ip_limit=$(grep ^DENY_IP_LIMIT /etc/csf/csf.conf|awk '{print $NF}'|cut -d'"' -f2)
if [[ ${ip_limit} -lt 7000 ]]; then
        echo "NOTE: csf.conf: DENY_IP_LIMIT is ${ip_limit}, if the flood has many IPs, this may need adjusting"
  else
        echo "NOTE: csf.conf: DENY_IP_LIMIT is ${ip_limit}"
fi

# initial flush to skip old entries
:>"${log_file}"
echo "starting in 5 seconds." ; sleep 5

sleep_time=$3
if [[ -z ${sleep_time} ]]; then
        sleep_time=30
fi

empty_run=1
empty_run_max=30
while true; do
        new_block=0
        log_parse_and_ban
        if [[ ${new_block} -eq 1 ]]; then
                csf -r
                empty_run=1
          else
                echo "nothing blocked, sleeping ${sleep_time}s (${empty_run} / ${empty_run_max})"
                (( empty_run++ ))
        fi

        # stop the script if there are no new IPs to block for next 30mins
        # and schedule removal of blocked IPs in 24 hours
        if [[ ${empty_run} -gt ${empty_run_max} ]]; then
                echo "no new blocks for ${empty_run_max} iterations, scheduling blocked IPs removal in 24hours"
                echo "sed -i '/log_flood_block.sh/d' /etc/csf/csf.deny && csf -r" |at now + 24 hours
                echo "done, exiting."
                exit 0
          else
                echo -e "\nsleeping ${sleep_time}s..\n"
                sleep "${sleep_time}"
        fi
done