#!/usr/bin/perl # This script looks through the apache log for a site and tabulates the number of # unique ip addresses accessing the site with each username. It then rewrites the # htaccess file with only the usernames that have not gone over the limit set herein # Copyright (C) 2005 Travis Morgan # Version 0.1 - April 11 2005 # 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. use strict; use Date::Manip; my $htaccess = '/var/www/html/members/.htpasswd'; # htpasswd file with users/passwords my $htaccess_suspended = '/var/www/html/members/.htpasswd.suspended'; # htpasswd file that will contain suspended users my $htaccess_tmp = '/tmp/limit_ips_per_user.tmp'; # temp file where good users will be written to my $apache_log = '/var/log/httpd/access_log'; # the apache log file for the site my $limit = 3; # number of unique ips allowed per username (remember how often your log rotates if period = 0) my $period = 15; # number of minutes the script should parse from the end of the log file. should match your cron time (0 is unlimited) my $debug = 0; # set this to 1 if you just want some output of what it WOULD do, but don't want to actually do it my $admin = 'address1@domain.com,address2@domain.com'; # address to send notices to my $sender = 'limit_ips_per_user@myserver.com'; # address emails should appear to have come from my $mailprog = '/usr/sbin/sendmail'; # what to use to send mail my $ignore_class_c = 0; # count requests from any address in the same class C as one ip address (useful for things like AOL proxies) ###### You shouldn't have to modify anything below ###### my @htaccess; # holds the entire htaccess file my $userline; # holds one line from the htaccess file my $user; # holds one user my $password; # holds the above user's password my @data; # array holding user names, passwords, ips my @logfile; # array holding the apache log my $line; # holds a single line from the apache log my $numusers = -1; # used to keep track of number of users in array my $anonymous = 0; # used to keep track of number of anonymous site accesses my $unknown = 0; # used to keep track of unknown usernames my $host; # host access the site my $dash; # - field my @trailing; # rest of log line that we don't use my $founduser; # boolean used to keep track of a user found my $foundhost; # boolean used to keep track of a host found my $curuser; # the current user in the data array my $curhost; # the current host in the users record in the data array my $mode; # the original file permissions my $uid; # the original file owner my $gid; # the original file group my $logtime; # time of the request my $logtz; # timezone the time is in my $time = time; # current unix time my @suspended; # suspended user email data my $suspended; # used in mail loop my @nonmember; # nonmember user email data my $nonmember; # used in mail loop $period = $period * 60; # check the original file permissions if ((-f $htaccess)&&(-r $htaccess)) { $mode = (stat($htaccess))[2] & 07777; $uid = (stat($htaccess))[4]; $gid = (stat($htaccess))[5]; } else { die "Cannot access $htaccess: $!\n"; } # read in the htaccess file open (HTACCESS, "<$htaccess") || die "Could not open $htaccess: $!"; @htaccess = ; close (HTACCESS); # create the initial data array holding username password pairs foreach $userline (@htaccess) { $numusers++; ($data[$numusers][0],$data[$numusers][1]) = split(/:/,$userline); $data[$numusers][2] = 0; # number of hosts for this user } # why are you running this script? if ($numusers == 0) { die "No users read.\n"; } # read the apache log file open (LOGFILE, "<$apache_log") || die "Could not open $apache_log: $!"; @logfile = ; close (LOGFILE); @logfile = reverse(@logfile); # fill the data array with ip addresses associated with each user foreach $line (@logfile) { # grab the variables needed from the next line of the array ($host, $dash, $user, $logtime, $logtz, @trailing) = split(/ /,$line); if ($ignore_class_c == 1) { $host =~ s/(.*)\..*/\1/; } if ($period != 0) { $logtime =~ s/^\[//; $logtz =~ s/]$//; $logtime = UnixDate(ParseDate($logtime." ".$logtz),"%s"); if ($time - $logtime >= $period) { last; } } if ($user eq '-') { $anonymous++; next; } # find username in data array $founduser = 0; $curuser = 0; while (($curuser <= $numusers)&&($data[$curuser][0] ne $user)) { $curuser++; } # if we found the right user if ($data[$curuser][0] eq $user) { # find the host $foundhost = 0; $curhost = 3; while (defined $data[$curuser][$curhost]) { if ($data[$curuser][$curhost] eq $host) { $foundhost = 1; } $curhost++; } # if the host wasn't already in the array for that user then add it if ($foundhost == 0) { $data[$curuser][$curhost] = $host; $data[$curuser][2]++; } } else { # add the user to the array so we can keep track of the hosts they accessed from $data[$curuser][0] = $user; $data[$curuser][1] = 'NONMEMBER'; $data[$curuser][2] = 0; $unknown++; $numusers++; } } # now the useful part.. check if the user has access from more that $limit hosts $curuser = 0; if ($debug != 1) { open(SUSPEND, ">>$htaccess_suspended") || die "Could not open $htaccess_suspended for writing: $!"; open(HTACCESS_TMP, ">$htaccess_tmp") || die "Could not open $htaccess_tmp for writing: $!"; } # write the suspended users out to the suspended file, and the good users to the temp file, and create the email data while ($curuser <= $numusers) { if ($data[$curuser][1] ne 'NONMEMBER') { if ($data[$curuser][2] > $limit) { if ($debug != 1) { printf SUSPEND $data[$curuser][0].':'.$data[$curuser][1]; } else { print "We should suspend $data[$curuser][0] for accessing from $data[$curuser][2] hosts!\n"; } $curhost=3; push @suspended, "\n".$data[$curuser][0]." - ".$data[$curuser][2]." hosts\n"; while (defined $data[$curuser][$curhost]) { push @suspended, " ".$data[$curuser][$curhost]."\n"; $curhost++; } } else { if ($debug != 1) { printf HTACCESS_TMP $data[$curuser][0].':'.$data[$curuser][1]; } } } else { $curhost=3; push @nonmember, "\n".$data[$curuser][0]." - ".$data[$curuser][2]." hosts\n"; while (defined $data[$curuser][$curhost]) { push @nonmember, " ".$data[$curuser][$curhost]."\n"; $curhost++; } } $curuser++; } # close the file handles, fix the permissions, and move the temporary .htaccess file into place as the new active file if ($debug != 1) { close(SUSPEND); close(HTACCESS_TMP); rename $htaccess_tmp, $htaccess || die "Could not move $htaccess_tmp to $htaccess: $!\n"; chmod $mode, $htaccess; chown $uid, $gid, $htaccess; chmod $mode, $htaccess_suspended; chown $uid, $gid, $htaccess_suspended; } else { print "Unknown users found : $unknown\n"; print "Anonymous accesses : $anonymous\n"; } $period = $period / 60; # send an informational email if (scalar @suspended + scalar @nonmember > 0) { open (MAIL, "| $mailprog $admin") || die "Could not open $mailprog: $!"; print MAIL "Reply-to: $sender\n"; print MAIL "From: $sender\n"; print MAIL "To: $admin\n"; print MAIL "Subject: Status report from the limit_ips_per_user.pl script\n\n"; if ($debug == 1) { print MAIL "\n******** DEBUG MODE ON - NO USERS WERE HARMED IN THE MAKING OF THIS EMAIL ********\n\n"; } if (scalar @suspended > 0) { print MAIL "The following users accessed the site from more than $limit IPs within a $period minute\n"; print MAIL "time period and were subsequently suspended.\n"; foreach $suspended (@suspended) { print MAIL "$suspended"; } } print MAIL "\n"; if (scalar @nonmember > 0) { print MAIL "The following users appeared in the log but not in the htpasswd file:\n"; foreach $nonmember (@nonmember) { print MAIL "$nonmember"; } } close (MAIL); }