#!/usr/local/bin/perl -w

# hawk version 0.60
# Greg Heim <gregheim@mindspring.com>

use strict;

use Socket;
use Net::Netmask;
use Net::Ping;
use File::Basename;
use DBI;

# Variables defined in the config file
our (@networks, @gateways, $frequency, $pingtimeout, $debuglevel, $logfile,
	$dbuser, $dbpass, $dbhost, $dbname, $pidfile);

############################################################################
# create pid file and process config file
############################################################################
readConfigs("/usr/local/etc/hawk/daemon.conf");

createPidFile($pidfile);

############################################################################
# signal handling
############################################################################

$SIG{HUP}='restart';
$SIG{INT}='cleanup';
$SIG{TERM}='cleanup';
$SIG{USR1}='debugUp';
$SIG{USR2}='debugDown';


##########################################################################
# main loop
##########################################################################
LOOP: while () {
   my ($block, @children, $netblock, $gateway, @ips, $ip, $pingtime, $hostname);

   my $loopstarttime = time();


   BLOCK: foreach $block (@networks) {

   my $pid = fork();
   if ($pid) {
      # parent
      push (@children, $pid);
   } elsif ($pid == 0) {
      # child

      $netblock = new Net::Netmask ($block);

      logMsg (2, "Forked a child process for $netblock");

      # check here that gateway is up
      if (@gateways) {
         foreach $gateway (@gateways) {

            logMsg (1, "Checking gateway $gateway in $block");

            if ($netblock -> match ($gateway)) {

               if (!ping($gateway, $pingtimeout)) {
                  logMsg (1, "No response from gateway $gateway, skipping network $block");
                  exit (0);
               }
               else {
                  logMsg (1, "Gateway $gateway responded.  $block ok so far.");
               }
            }
            else {
               # gateway not in this block
               logMsg (1, "Gateway $gateway not in $block network");
            }
         }
      }

      logMsg (1, "Checking $block");
      @ips = $netblock -> enumerate();

      foreach $ip (@ips) {


         # skip broadcast addresses
         if ($ip eq $netblock -> base()) {
            logMsg (1, "Skipping old style broadcast $ip");
            next;
         }
         if ($ip eq $netblock -> broadcast()) {
            logMsg (1, "Skipping broadcast $ip");
         }


         # get on with it
         if (ping($ip, $pingtimeout)) {
            $pingtime = time();
         }
         else {
            $pingtime = 0;
         }
         $hostname = gethostbyaddr (inet_aton($ip), AF_INET);
         $hostname = "" unless $hostname;

	 my $for_rev_match='0';
	 if ($hostname) {
	    my $packed_host_ip = gethostbyname("$hostname");
	    if (defined $packed_host_ip) {
	       if (inet_ntoa($packed_host_ip) =~ /^$ip$/) {
	          $for_rev_match='1';
	       }
	    }
	 }

         # check if it's already in the database
         # if pingtime is zero, that field isn't updated
         if (ipExists($ip)) {
            updateRecord($ip, $hostname, $pingtime, $for_rev_match);
         }
         else {
            insertRecord($ip, $hostname, $pingtime, $for_rev_match);
         }
      }

      logMsg (2, "Child process for $netblock exiting");
      exit (0);

   } else {
      die "Couldn't fork: $!\n";
   }

   }

   foreach (@children) {
      waitpid($_, 0);
   }

   # sleep until it's time for another run
   logMsg (1, "Waiting for next run...");
   while () {

      if ($loopstarttime + $frequency > time()) {
         sleep 1;
      }
      else {
         next LOOP;
      }
   }
}
exit;


##########################################################################
# determine if the ip address is already in the database
##########################################################################
sub ipExists {

   my ($ip) = @_;
   my $qip; # ip from query
   my ($dbh, %attr, $sth);

   logMsg (2, "Checking database for $ip");

   $dbh = DBI->connect('dbi:mysql:' . $dbname . ';host='. $dbhost, $dbuser, $dbpass, \%attr) or die "Couldn't connect to database: " . DBI->errstr;
   $sth = $dbh -> prepare("select ip from hawk.ip where ip='$ip'");
   $sth -> execute or logMsg (0, "Couldn't do select for IP" . DBI -> errstr);
   $sth -> bind_columns( \($qip) );
   $sth -> fetch;
   if ($qip) {
      logMsg (2, "$ip exists");
      return 1;
   }
   else {
      logMsg (2, "$ip doesn't exist");
      return 0;
   }
}



##########################################################################
# create a new ip record
##########################################################################
sub insertRecord{

   my ($ip, $hostname, $pingtime, $for_rev_match) = @_;
   my ($dbh, %attr, $sth);

   logMsg (2, "Inserting record: IP: $ip, Hostname: $hostname, Pingtime: $pingtime, Match: $for_rev_match");

   $dbh = DBI->connect('dbi:mysql:' . $dbname . ';host='. $dbhost, $dbuser, $dbpass, \%attr) or die "Couldn't connect to database: " . DBI->errstr;
   $sth = $dbh -> prepare("insert into hawk.ip (ip, hostname, lastping, for_rev_match) values ('$ip', '$hostname', '$pingtime', '$for_rev_match')");
   $sth -> execute or logMsg (0, "Couldn't update record!" . DBI -> errstr);

}


##########################################################################
# update the ip record.  if 0 pingtime is passed, that field isn't updated
##########################################################################
sub updateRecord{

   my ($ip, $hostname, $pingtime, $for_rev_match) = @_;
   my ($query, $dbh, %attr, $sth);

   logMsg (2, "Updating record: IP: $ip, Hostname: $hostname, Pingtime: $pingtime, Match: $for_rev_match");

   if ($pingtime) {
      $query = "update hawk.ip set hostname = '$hostname', lastping = '$pingtime', for_rev_match = '$for_rev_match' where ip = '$ip'";
   }
   else {
      $query = "update hawk.ip set hostname = '$hostname', for_rev_match = '$for_rev_match' where ip = '$ip'";
   }

   $dbh = DBI->connect('dbi:mysql:' . $dbname . ';host='. $dbhost, $dbuser, $dbpass, \%attr) or die "Couldn't connect to database: " . DBI->errstr;
   $sth = $dbh -> prepare($query);
   $sth -> execute or logMsg (0, "Couldn't insert record!" . DBI -> errstr);
}



##########################################################################
# ping subroutine, takes an ip address and a timeout value (from .conf)
##########################################################################
sub ping {

   my ($ip, $timeout) = @_;
   my ($p, $return);

   logMsg(2, "Pinging $ip");

   $p = Net::Ping->new('icmp');
   $return = $p->ping($ip, $timeout);
   $p->close;

   logMsg(2, "Ping $ip returned $return");

   return $return;
}



##########################################################################
# write pid file
##########################################################################

sub createPidFile {

   my ($pidfile) = @_;

   logMsg (2, "Creating pid file $pidfile");

   # see if pid file exists
   if ( -e $pidfile ) {

      my $pid = `cat $pidfile`;

      # found pid file.  signalling existing process
      logMsg (1, "Pid file $pidfile exists.  Attempting to signal existing process.");
      my $kstatus = kill 0, $pid;

      # see if we killed anything
      if ($kstatus) {
         logMsg (1, "Daemon appears to already be running.  We can exit.");
         exit 0;
      }
      else {
         logMsg (1, "Process not running.  Removing old pidfile.");
         unlink $pidfile || logMsg(1, "Couldn't remove pid file $pidfile\n");
      }
   }

   # create pid file
   open PIDFILE, "> $pidfile" || die "Couldn't create $pidfile: $!\n";
   print PIDFILE "$$";
   close PIDFILE;

}



##########################################################################
# restart
##########################################################################

sub restart {

   logMsg (1, "Recieved HUP.  Restarting.");

   # remove pid file
   unlink $pidfile || logMsg(1, "Couldn't remove pid file $pidfile\n");
   exec $0;
}



##########################################################################
# cleanup and exit
##########################################################################

sub cleanup {

   logMsg (1, "Killed.  Cleaning up.");

   $SIG{TERM}='IGNORE';
   $SIG{INT}='IGNORE';

   # remove pid file
   unlink $pidfile || logMsg(1, "Couldn't remove pid file $pidfile\n");
   exit 0;

}



##########################################################################
# step up debug level
##########################################################################
sub debugUp {

   $debuglevel++;
   logMsg($debuglevel, "New debug level: $debuglevel");

}



##########################################################################
# step down debug level
##########################################################################
sub debugDown {

   $debuglevel--;
   logMsg($debuglevel, "New debug level: $debuglevel");

}



##########################################################################
# process the config file
##########################################################################

sub readConfigs {

   my ($file) = @_;

   do $file;

}



##########################################################################
# write debug messages
##########################################################################
sub logMsg {

   my ($level, $message) = @_;
   if ($debuglevel >= $level) {

      my $time = localtime(time);
      open LOGFILE, ">> $logfile" || die "Couldn't open logfile: $!\n";
      print LOGFILE "$time: $message\n";
      close LOGFILE;
   }
}
