#!/bin/bash
#
# This script is a quick "hack" that will watch the apache error log for
# "authentication failure" messages, and will disable the associated user
# account in .htpasswd if the number of auth failures for that user
# exceeds a set threshold during the given monitoring interval.  The
# account will be re-enabled after a lock-out period has passed, unless
# it has continued to rack up excessive authentication failures during
# the lock-out period.
#
# The idea is to enable use of HTTP(S) Basic Auth, but make it difficult
# to brute-force a password, which is a classic weakness of Basic Auth.
# It's a quick script that meets a specific need, and probably has a few
# bugs, although it's been used in production in a couple of places.
# 
# Paul Kreiner, 01/24/2008


# Number of seconds per monitoring interval
INTERVAL=30
# Maximum failed attempts in the above interval before we lock the account
MAXTRIES=5
# How long to disable an account once it gets locked
FAILWAIT=90
# What file to we monitor for errors?
MONITOR_FILE=/var/log/apache2/error.log
# What is the string we look for?
MONITOR_STRING="authentication failure"
# What htpasswd file will we modify to lock out accounts?
HTPASSWD=/var/www/.htpasswd

# Temporary work files, used internally
TMP_PID_FILE=`tempfile -p htpas`
INTERVAL_ERROR_FILE=`tempfile -p htpas`
INTERVAL_ERROR_FILE_2=`tempfile -p htpas`
TMPFILE=`tempfile -p htpas`

umask 077
IFS=''
( bash -c "echo \$\$ > $TMP_PID_FILE ; tail --pid $$ -p `cat z` -q -f -n0 $MONITOR_FILE 2>/dev/null |\
    grep --line-buffered \"$MONITOR_STRING\" > $INTERVAL_ERROR_FILE 2>/dev/null" & ) >/dev/null 2>&1

# Eternal loop...
while [ 1 ]; do
  sleep $INTERVAL
  pkill -P `cat $TMP_PID_FILE` > /dev/null 2>&1
  kill `cat $TMP_PID_FILE` > /dev/null 2>&1
  mv $INTERVAL_ERROR_FILE $INTERVAL_ERROR_FILE_2
  ( bash -c "echo \$\$ > $TMP_PID_FILE ; tail --pid $$ -f -n0 $MONITOR_FILE |\
      grep --line-buffered \"$MONITOR_STRING\" > $INTERVAL_ERROR_FILE 2>/dev/null" & ) >/dev/null 2>&1
  
  # Get the current time (and when any locks that we set should expire)
  NOW=`date +%s`
  NOWH=`date`
  (( LATER = $NOW + $FAILWAIT ))
  LATERH=`date -d @${LATER}`

  # Enable any previously-disabled accounts that have passed the waiting period.
  for LINE in `egrep --line-buffered '^#[0-9]{10}#' $HTPASSWD |\
                 awk -vnow=$NOW -F# \
                   'NF>2{if($2<=now) {print $3}}'`; do
    echo "[${NOWH}] Re-enabling: $LINE"
    cat $HTPASSWD | sed -re "s/^#[0-9]{10}#${LINE}/${LINE}/" > $TMPFILE
    chmod 0644 $TMPFILE
    chown root:root $TMPFILE
    mv -f $TMPFILE $HTPASSWD
  done

  # Disable the accounts we've just flagged
  for LINE in `cat $INTERVAL_ERROR_FILE_2 | cut -d\  -f10 | tr -d : |\
                 sort | uniq -c | awk -vmaxtries=$MAXTRIES \
                                    '{if ($1>maxtries) {print $2}}'`; do
    grep -q --line-buffered -E "^${LINE}" $HTPASSWD > /dev/null 2>&1 && {
      echo "[${NOWH}] Disabling: $LINE until [$LATERH]."
      cat $HTPASSWD | sed -re "s/^${LINE}/#${LATER}#${LINE}/" > $TMPFILE
      chmod 0644 $TMPFILE
      chown root:root $TMPFILE
      mv -f $TMPFILE $HTPASSWD
    }
  done
  rm -f $INTERVAL_ERROR_FILE_2
done
