#!/usr/bin/perl -w
#
# apache-protect - Apache DoS protection using mod_status and blacklist
# Apache Security, http://www.apachesecurity.net
# Copyright (c) 2004 Ivan Ristic <ivanr@webkreator.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#

# 
# This script analyzes the mod_status output and tries to
# figure if any of the IP addresses are attempting to perform
# a denial of service attack against the web server. If an
# attacker is discovered it is blacklisted using the blacklist
# utility.
#
# Change the uppercase variables below and add the script to
# cron to execute every minute.
#

use LWP::UserAgent;

# The mod_status page
$URL = "http://localhost/server-status/";

# Where is the blacklist binary.
$BLACKLIST = "/sbin/blacklist";

# How long to block offending IP addresses for.
$BLOCK_DURATION = 3600;

# How many requests do we need to get to treat
# them as an attack.
$DEFAULT_THRESHOLD = 5;

# By default, apache-protect defends against IP addresses
# that send identical requests. By setting $UNSAFE to 1
# it does not take the URL into account. This can be
# dangerous as it can produce false positives for some
# genuine clients with many requests in parallel.
$UNSAFE1 = 0;

# Set this to 1 if you want the script to count the
# requests that are completed - this can be useful for
# DoS attacks that finish quickly, e.g. invalid requests.
$UNSAFE2 = 0;

# An overriding threshold list. The value -1 means never
# block. Any other value establishes a threshold for
# the given IP address.
%WHITELIST = ( "127.0.0.1" => -1 );


sub get_status_page {
    my $url = shift(@_);

    my $ua = new LWP::UserAgent;
    $ua->timeout(30);
    $ua->agent("apache-protect/1.0");

    my $request = HTTP::Request->new(GET => $url);
    my $response = $ua->request($request);

    if (!$response->is_success) {
        print "Failed: ", $response->status_line, "\n";
        return 0;
    }

    return $response->content;
}

$page = get_status_page($URL);
if ($page eq "0") {
    exit();    
}

$i1 = index($page, "<table");
$i2 = index($page, "</table");
if (($i1 == -1)||($i2 == -1)) {
    print("You must configure ExtendedStatus On in httpd.conf\n");
    exit();
}
$page = substr($page, $i1, $i2 - $i1);

$i1 = index($page, "</tr>");
$page = substr($page, $i1 + 5);

$page =~ s/\n/ /g;
$page =~ s/<\/tr>/\n/g;
$page =~ s/<tr>/ /g;
$page =~ s/<b>//g;
$page =~ s/<\/b>//g;
$page =~ s/ nowrap//g;
$page =~ s/<\/font>//g;
$page =~ s/<\/td>//g;
$page =~ s/<font[^>]+>//g;

%ips = ();

foreach $line (split("\n", $page)) {
    # print $line . "\n";
    @tokens = split("<td>", $line);
    shift(@tokens);

    if ((@tokens == 13)
      && ( ($UNSAFE2 == 1)||( (!($tokens[3] eq "_ "))&&(!($tokens[3] eq ". ")) ) )
      &&(!($tokens[3] eq "S "))
      &&(!($tokens[3] eq "I ")) ) {
        $ip = $tokens[10];
        # print $ip . " " . $tokens[12] . "\n";

        if (!($ip eq "?")) {
            my($key);

            if ($UNSAFE1) {
                $key = $ip;
            } else {
                $key = $ip . "," . $tokens[12];
            }

            if (exists $ips{$key}) {
                $ips{$key}++;
            } else {
                $ips{$key} = 1;
            }
        }
    }
}

while(($key, $count) = each %ips) {
        
    if (index($key, ",") != -1) {
        $ip = substr($key, 0, index($key, ","));
    } else {
        $ip = $key;
    }

    $threshold = $DEFAULT_THRESHOLD;
    if (exists $WHITELIST{$ip}) {
        $threshold = $WHITELIST{$ip};
    }   
    if (($threshold != -1)&&($count >= $threshold)) {
        print "Blacklisted $ip (count=$count) for $BLOCK_DURATION seconds\n";
        system("$BLACKLIST block $ip $BLOCK_DURATION");
    }
}
