#!/usr/bin/perl -w
#
# Dec 31, 1999 Leif Sawyer
#   LS- A quick hack to find a IP/MAC pair from the DHCP server
#       via WWW interface.
#
# Jan 3, 2000 Jason Richards
#   JR- Added support for compressed log files
#
# Jan 4, 2000 Leif Sawyer
#   LS- Added code to 'subtract a day' so that logs are parsed
#       for the correct date, not the day-behind that logs normally are.
#       Also color-coded the dhcp status for each entry so that problems
#       are easily found.  We're treating DHCPDISCOVER's as unnecessary info.
#
# Jan 6, 2000 Jason Richards, Leif Sawyer
#   JR- Modularized the parsing of the dhcp logs to remove the duplicate code.
#   JR- Added extra DHCP action fields, and fixed a typo
#   LS- Also cleaned up the colors in order to better illustrate the flow.

use Compress::Zlib;
use Time::Local;

$AppName = "FindHosts";
$Version = "2.1";

$DEBUG = 0;

$CurLogFile = "/var/log/dhcpd.log";
$OldLogDir = "/var/log/old";
@months=(Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec);

print "Content-type: text/html\n\n";
print "<!-- I am $0 -->\n";

&ReadParse;

if (! keys %in) {
	&Form;
}

@required_fields = ("host", "date", "search", split(/\s*,\s*|\000/, $in{'required_fields'}));
@missing_fields = grep ($in{$_} !~ /\S/, @required_fields);

if (@missing_fields) {
	$errormessage = "You did not provide sufficient information.\nYou are required to fill out the following:\n\n<UL>";
	$errormessage .= join("\n<LI> ", "", @missing_fields);
	$errormessage .= "\n</UL>\n\nPlease go back and fill out the form again.\n";
	&Exit("Insufficient Information", $errormessage);
}

# Untaint so we don't get nasty shell metacharacters.
$myhost = $in{"host"};
$host_orig = $myhost;
$myhost =~ /^([\w\:\.]*)$/;  $myhost = $1; # Untaint it
$myhost =~ s/^\s+//; s/\s+$//;
&Exit("Warning: Illegal usage.", "Illegal characters found in \"host\" variable.") if ($myhost ne $host_orig);

print qq|<!-- Using search = '$in{"search"}' -->\n|;
if ($in{"search"} eq "exact") {
	$search = "\\b" . $myhost . "\\W";
} else {
	$search = $myhost;
}

$unknown_color = "7F7F7F";	# Grey
$ack_color = "00FF40";		# Green
$nack_color = "FF0040";		# Red
$offer_color = "A0A0F0";	# Light Purple
$request_color = "A0F0F0";	# Light Blue
$release_color = "FFA0FF";	# Light Pink
$inform_color = "800090";	# Purple
$decline_color = "FF00C0";	# Magenta
$reclaim_color = "FFE000";	# Dark Yellow
$abandon_color = "FF8000";	# Orange

$date = $in{"date"};
print "<!-- Date = '$date' -->\n";
#####
#
# Read and parse the logfile.
#

my($v) = 0;

if ($date == "Current") {
	open(DATA,"<$CurLogFile") || &Exit("Fatal Error", "Can't open logfile $logfile!<br>Contact <a href=\"mailto:root\@gci.net\">root\@gci.net</a>");
	while(<DATA>) {

		&BuildInfo($_);

	}
	close (DATA);
} else {
	$logfile = $OldLogDir . "/dhcpd.log-" . $date . ".gz";

	$gz = gzopen($logfile, "rb") || &Exit("Fatal Error", "Can't open logfile $logfile: $gzerrno<br>Contact <a href=\"mailto:root\@gci.net\">root\@gci.net</a>");

	while($gz->gzreadline($_) > 0) {

		&BuildInfo($_);

	}
	$gz->gzclose();
}

$| = 1;

	print qq|
<html>
<!-- $AppName v$Version - Copyright December 1999 Leif Sawyer -->
<!-- email: leif\@gci.net -->
<head><title>DHCP Host info for: $myhost</title></head>
<body bgcolor="ffffff" text="000000">
<center><h2>DHCP Host info for: <b>$myhost</b></h2>
<hr width="50%">
</center>
<table border=0 align=center>
<tr>
 <td align=center bgcolor=$offer_color>Server Offer</td>
 <td>&nbsp;</td>
 <td align=center bgcolor=$request_color>Client Request</td>
</tr>
<tr>
 <td align=center bgcolor=$ack_color>Server ACK</td>
 <td>&nbsp;</td>
 <td align=center bgcolor=$nack_color>Client/Server NACK</td>
</tr>
<tr>
 <td align=center bgcolor=$decline_color>Client Decline</td>
 <td>&nbsp;</td>
 <td align=center bgcolor=$release_color>Client Release</td>
</tr>
<tr>
 <td align=center bgcolor=$abandon_color>Address Abandoned</td>
 <td>&nbsp;</td>
 <td align=center bgcolor=$reclaim_color>Address Reclaimed</td>
</tr>
<tr>
 <td align=center bgcolor=$inform_color>Client Info Request</td>
 <td>&nbsp;</td>
 <td align=center bgcolor=$unknown_color>Unknown DHCP action</td>
</tr>
</table>
<hr>
<br>
<TABLE BORDER=1 ALIGN=center WIDTH=100%>
<TR>
 <td align=center bgcolor=000060><b><font color=ffffff>Mon/Day</td>
 <td align=center bgcolor=000060><b><font color=ffffff>Time</td>
 <td align=center bgcolor=000060><b><font color=ffffff>IP Address</td>
 <td align=center bgcolor=000060><b><font color=ffffff>MAC Address</td>
</TR>|;

	foreach $v ( sort bynum keys %data ) {

# Post Processing the data...

		print qq|
<TR>
 <td bgcolor=FFFFA0 align=center><b>$data{$v}{"date"}</b></td>
 <td bgcolor=FFFFA0 align=right>$data{$v}{"time"}</td>
 <td bgcolor=FFFFFF align=right>$data{$v}{"hostip"}</td>
 <td bgcolor=$data{$v}{"type"} align=right>$data{$v}{"hostmac"}</td>
</TR>|;
	}

	print qq|
</table><p><p>
<font size="-1">
$AppName version:$Version<br>
<a href="http://freshmeat.net/search.php3?query=$AppName">Latest Version</a>
</font>
</body></html>
|;


	exit 0;

##### End of Main Program
########################################################################

sub ReadParse {
    local (*in) = @_ if @_;

    local ($i, $key, $val);

    # Read in text
    if    ($ENV{'REQUEST_METHOD'} eq "GET") { $in = $ENV{'QUERY_STRING'}; }
    elsif ($ENV{'REQUEST_METHOD'} eq "POST") { read(STDIN,$in,$ENV{'CONTENT_LENGTH'}); }

    @in = split(/&/,$in);

    foreach $i (0 .. $#in) {
	# Convert plus's to spaces
	$in[$i] =~ s/\+/ /g;

	# Split into key and value.  
	($key, $val) = split(/=/,$in[$i],2); # splits on the first =.

	# Convert %XX from hex numbers to alphanumeric
	$key =~ s/%(..)/pack("c",hex($1))/ge;
	$val =~ s/%(..)/pack("c",hex($1))/ge;

	# Associate key and value
	$in{$key} .= "\000" if (defined($in{$key})); # \0 is the multiple separator
	$in{$key} .= $val;
    }

    return 1; # just for fun
}

sub BuildInfo {
	if (/$search/) {

		next if m/DHCPDISCOVER/i;
	
		my $line = $_;
		my($month,$day,$time,$hostname,$service,$action,$verb1,$hostip,$verb2,$hostmac,$how,$relay) = split /\s+/,$line;
	
		$data{$v}{"date"} = $month . " " . $day;
		$data{$v}{"time"} = $time;
		$data{$v}{"type"} = $unknown_color;

		if ($hostip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
			$data{$v}{"hostip"} = $hostip;
		} elsif ($line =~ /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/) {
			$data{$v}{"hostip"} = $1;
		}

		if ($hostmac =~ /^\w{2}\:\w{2}\:\w{2}\:\w{2}\:\w{2}\:\w{2}$/) {
			$data{$v}{"hostmac"} = $hostmac;
		} elsif ($line =~ /(\w{2}\:\w{2}\:\w{2}\:\w{2}\:\w{2}\:\w{2})$/) {
			$data{$v}{"hostmac"} = $1;
		} else {
			$data{$v}{"hostmac"} = "No MAC address logged";
		}
	
		print "<!-- $hostip action: $action -->\n" if $DEBUG;
	
		if ($action eq "DHCPACK") {
			$data{$v}{"type"} = "$ack_color";
		} elsif ($action eq "DHCPNAK") {
			$data{$v}{"type"} = "$nack_color";
		} elsif ($action eq "DHCPREQUEST") {
			$data{$v}{"type"} = "$request_color";
		} elsif ($action eq "DHCPOFFER") {
			$data{$v}{"type"} = "$offer_color";
		} elsif ($action eq "DHCPRELEASE") {
			$data{$v}{"type"} = "$release_color";
		} elsif ($action eq "DHCPDECLINE") {
			$data{$v}{"type"} = "$decline_color";
		} elsif ($action eq "DHCPINFORM") {
			$data{$v}{"type"} = "$inform_color";
		} elsif ($action eq "Abandoning" ) {
			$data{$v}{"type"} = "$abandon_color";
		} elsif ($action eq "Reclaiming" ) {
			$data{$v}{"type"} = "$reclaim_color";
		}
	
		$v++;
	}

	return $v;
}

sub Exit {
    local($errorheader) = shift(@_);

    print "<TITLE>$AppName: $errorheader</TITLE>\n";
    print "<body>";
    print "<H1>$errorheader</H1>\n";
    print @_;
    print "<P><HR>Unable to complete action.\n";

    exit(2);
}

sub Form {
print qq|
<html>
<!-- $AppName v$Version - Copyright December 1999 Leif Sawyer for GCI Communications -->
<!-- email: leif\@gci.net -->
<head><title>DHCP Host info finder</title></head>
<body bgcolor="ffffff" text="000000">
<center>
	<h2>DHCP Host info finder</h2>
<hr widht="70%">
</center>
Please enter an IP address or MAC address, then click on the submit button!<br>
You may also choose to search historical data by selecting a date from the dropbox.<br>
<blockquote>IP Addresses should be of the form: 192.168.22.1<br>
MAC Addresses should be of the form: ab:00:1d:0e:ff:c7
</blockquote>
<form action="/cgi-bin/$AppName" method=POST>
Search <select name="date">
<option value="current" selected>Today\n\n|;
opendir(MDIR, "$OldLogDir") or die "Can't open $OldLogDir: $!";
@allfiles=readdir(MDIR);
closedir(MDIR);
foreach (reverse sort @allfiles) {
        if (m/.*dhcpd\.log-(\d\d\d\d)(\d\d)(\d\d)\.gz/i) {
		$year = $1; $month = $2; $mday = $3;
		$file = $year . $month . $mday;
		$timestamp = &timelocal(0,0,0,$mday,$month - 1,$year - 1900);
		$timestamp -= 86400;
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime($timestamp);
		$year += 1900;
		$date = $months[$mon] . " " . $mday . ", " . $year;
                print qq|<option value="$file">$date\n|;
        }
}

print "</select>\n";
print qq|<input type="text" width="20" name="host"></input>
<blockquote>
<input type=submit value="Find Host">&nbsp;&nbsp;&nbsp;
<select name="search">
<option value="exact" selected>Exact matches only
<option value="partial">Partial matches
</select>
</blockquote>
<hr>
<p><p>
<font size="-1">
$AppName version:$Version<br>
<a href="http://freshmeat.net/search.php3?query=$AppName">Latest Version</a>
</font>
</body></html>
|;

exit;
}

sub bynum { $a <=> $b; }

sub commas {
	local($_) = @_;
	1 while s/(.*\d)(\d\d\d)/$1,$2/;
	$_?$_:"0";
}
