#!/usr/bin/perl -w

# ISpy - a network monitoring tool
#
# Copyright (C), 2001 Alexander Hajnal
#
# You may distribute under the terms of the GNU General Public
# License as specified in the file COPYING that comes with the
# ISpy distribution.
#
# Sun Jul 29 19:00:53 EDT 2001  akh (ahajnal@interport.net) 

use IO::Socket;
use Getopt::Long;
use strict;

use vars qw / $opt_config $opt_state $opt_reset $opt_test 
              $opt_verbose $opt_help $opt_debug $opt_version /;
use vars qw / $ISpyConf $ISpyState $alert $maxlen $alias 
              $portname $portnum $watch $state $fail $status /;

use constant FALSE => 0;
use constant TRUE => 1;

$opt_config = '';
$opt_state  = '';
$opt_reset  = FALSE;
$opt_test   = FALSE;
$opt_help   = FALSE;
$opt_debug  = FALSE;
$opt_version= FALSE;

Getopt::Long::Configure('no_ignore_case', 'no_auto_abbrev');
$opt_help = 1 unless (GetOptions( "config|c=s", "state|s=s", "reset|r!", 
                                  "help|h|?!", "debug|d|verbose|v", "version|V"));

if ($opt_version == TRUE) {
  print "ISpy v.1.0 Copyright (c) 2001 Alexander Hajnal ahajnal\@interport.net\n";
	exit;
}

if ($opt_help == TRUE) {
	ShowUsage();
	exit;
}

($ISpyConf,$ISpyState) = GetFilenames($opt_config,$opt_state);
DEBUG("config='$ISpyConf', state='$ISpyState'");

($alert,$maxlen,$alias,$portname,$portnum,$watch) = ReadConfigFile($ISpyConf);

($state) = ReadStateFile($ISpyState);

($fail,$status)=CheckHosts($watch,$portnum);

WriteStateFile($ISpyState, $status);

SendReports($status,$state,$alert,$maxlen,$alias,$portname,$portnum) unless ($opt_reset);

sub DEBUG {
	print "DEBUG: $_[0]\n" if ($opt_debug);
}

sub ShowUsage {
	print <<EOF;
Usage:
ispy [--config|-c filename]
     [--state|-s filename] 
     [[--reset|-r]|[--noreset|-nor]]
     [-d|--debug|-v|--verbose]
     [-V|--version]
     [-h|--help]
EOF
}

sub GetFilenames {
	my($config,$state)=@_;
	DEBUG("determining filenames");
	if ($config ne '') { # User supplied config file
		DEBUG("user supplied config file '$config'");
		if ($state eq '') { # No state file specified
			if ($config =~ /^(.+)\/[^\/]+?$/) { # find config path
				$state = $1.'ispy.state';
			} else {
				$state = 'ispy.state';
			}
		}
		if (-e $config) {
			if ( ! -f $config ) {
				die "$config is not a file";
			}
			if ( ! -e $state ) {
				open STATE, ">$state" or die "Could not create state file $state\n";
				close STATE;
			} elsif ( ! -f $state ) {
				die "$state is not a file";
			}
			return ($config, $state);
		} else {
			die "Config file $config not found\n";
		}
		
	} elsif (-e "$ENV{HOME}/ispy.conf") { # Look in home directory
		DEBUG("config file in ~/");
		if ( ! -f "$ENV{HOME}/ispy.conf" ) {
			die "$ENV{HOME}/ispy.conf is not a file";
		}
		$state = "$ENV{HOME}/ispy.state" unless ($state);
		if ( ! -e $state) {
			open STATE, ">$state" or die "Could not create state file $state\n";
			close STATE;
		} elsif ( ! -f $state ) {
			die "$state is not a file";
		}
		
		return ("$ENV{HOME}/ispy.conf", $state);
		
	} elsif (-e "/etc/ispy.conf") { # Look in /etc
		DEBUG("config file in /etc/");
		$state = "/etc/ispy.state" unless ($state);
		if ( ! -e $state ) {
			open STATE, ">$state" or die "Could not create state file $state\n";
			close STATE;
		} elsif ( ! -f $state ) {
			die "$state is not a file";
		}
		
		return ("/etc/ispy.conf", $state);
		
	} elsif (-e "ispy.conf") { # Look in current directory
		DEBUG("config file in current directory");
		$state = "ispy.state" unless ($state);
		if ( !-e $state ) {
			open STATE, ">$state" or die "Could not create state file $state\n";
			close STATE;
		} elsif ( ! -f $state ) {
			die "$state is not a file";
		}
		
		return ("ispy.conf", $state);
		
	} elsif ($state ne '') { # User supplied state file but not config file
		DEBUG("config file not found, attempting to derive");
		if ($state =~ /^(.+)\/[^\/]+?$/) { # find state path
			$config = $1.'ispy.conf';
		} else {
			$config = 'ispy.conf';
		}
		if (-e $state) {
			if ( ! -f $state ) {
				die "$state exists but is not a file";
			}
			if ( ! -e $config ) {
				die "Could not find configuration file $config\n";
			} elsif ( ! -f $config ) {
				die "$config exists but is not a file";
			}
			return ($config, $state);
		} else {
			die "No configuration or state files found\n";
		}
	}
	die "ispy.conf and ispy.state not found (checked /etc, ~/ and current directory\n";
}

sub ReadConfigFile {
	my $ISpyConf = $_[0];
	DEBUG("reading config file");
	my %alert = ();
	my %maxlen = ();
	my %alias = ();
	my %portname = ();
	my %portnum = ();
	my @watch = ();
	my $line;
	open CONF, $ISpyConf or die "Could not open configuration file '$ISpyConf'\n";
	while ($line=<CONF>) {
		$line =~ s/\r|\n//g;
		if ($line =~ /^\s*(#.*)?$/) {
			 # NOOP
		} elsif ($line =~ /^alert\s+(\S.*)\s+(\S.*)$/) {
			# alert alias name@host
			$alert{$1} = $2;
		} elsif ($line =~ /^maxlen\s+(\S.*)\s+(\S.*)$/) {
			# maxlen account length
			# length = 0 -> no limit
			$maxlen{$1} = $2;
		} elsif ($line =~ /^alias\s+(\S.*)\s+(\S.*)$/) {
			# alias longname shortname
			$alias{$2} = $1;
		} elsif ($line =~ /^port\s+(\S.*)\s+(\S.*)$/) {
			# port number name
			$portname{$1} = $2;
			$portnum{$2} = $1;
		} elsif ($line =~ /^watch\s+(\S.*)\s+([^\/].*)\/([^\/]+$)/) {
			# watch host port/protocol
			push @watch, [$1,$2,$3];
		} else {
			print STDERR "Bad configuration line: $line\n";
		}
	}
	close CONF;
	return (\%alert,\%maxlen,\%alias,\%portname,\%portnum,\@watch);
}

sub ReadStateFile {
	my $ISpyState = $_[0];
	DEBUG("reading state file");
	my %state = ();
	my $line;
	open STATE, $ISpyState or die "Could not open configuration file '$ISpyState' for reading\n";
	while ($line=<STATE>) {
		$line =~ s/\r|\n//g;
		if ($line =~ /^\s*(#.*)?$/) {
			 # NOOP
		} elsif ($line =~ /^(\S+)\s+:\s+(\S.*)\s+:\s+(\S.*)\s+:\s+(\S.*)$/) {
			# state host port status
			$state{"$2:$3:$4"}=$1;
		} else {
			print STDERR "Bad state file line: $line\n";
		}
	}
	close STATE;
	return (\%state);
}

sub WriteStateFile {
	my ($ISpyState, $curstate) = @_;
	DEBUG("writing state file");
	my ($host, $state);
	rename $ISpyState, "$ISpyState.bck";
	open STATE, ">$ISpyState" or die "Could not open configuration file '$ISpyState' for writing\n";
	print STATE "# ISpy state file v1.0\n";
	print STATE "# This is a generated file, do not edit\n";
	print STATE "# As of ".localtime().":\n";
	foreach $host (sort keys %$curstate) {
		foreach $state(@{$$curstate{$host}}) {
			print STATE "$$state[0] : $$state[1] : $$state[2] : $$state[3]\n"
		}
	}
	close STATE;
}

sub CheckHosts {
  my ($watchlist,$portnum)=@_;
	DEBUG("checking hosts");
	my %fail=();
	my %status=();
	my ($watch,$hostname,$SOCKET);
	
	foreach $watch(@$watchlist) {
		my ($host,$port,$protocol)=@$watch;
		if (defined $$alias{$host}) {
			$hostname = $$alias{$host};
		} else {
			$hostname=$host;
		}
		$port = $$portnum{$port}  if (defined $$portnum{$port});
		unless ( $SOCKET=new IO::Socket::INET(PeerAddr => $hostname, 
																					PeerPort => $port,
																					Proto    => $protocol) )	{
			# failed to connect
			DEBUG("host: $host, port: $port, protocol: $protocol - FAIL");
			push @{$fail{$host}}, $watch;
			push @{$status{$host}}, ['-', $$watch[0], $$watch[1], $$watch[2]];
		} else {
			DEBUG("host: $host, port: $port, protocol: $protocol - OK");
			push @{$status{$host}}, ['+', $$watch[0], $$watch[1], $$watch[2]];
		}
	}
	return (\%fail,\%status);
}

sub SendReports {
	my ($curhash,$lasthash,$alert,$maxlen,$alias,$portname,$portnum) = @_;
	DEBUG("sending reports");
	my $subject = 'ISpy status'; 
	my $body='';
	my $host='';
	my $global_delta = FALSE;
	my $delta = FALSE;
	my ($watch, $line, $email, $text, $spec, $cur, $last);
	my %msg=();
	my %curtext=();
	my %shown=();
	foreach $host (sort keys %$curhash) {
		foreach $watch(@{$$curhash{$host}}) {
			$spec="$$watch[1]:$$watch[2]:$$watch[3]";
			$$lasthash{$spec} = '+' unless (defined $$lasthash{$spec});
			DEBUG("Current: $$watch[0] $spec");
			DEBUG("Last:    ".$$lasthash{$spec}." $spec");
			$cur=$$watch[0];
			$last=$$lasthash{$spec};
			if (($cur eq '+') && ($last eq '-')) {
				unless (defined $shown{$host}) {
					$shown{$host} = TRUE;
					foreach $email(keys %$alert) {
						$curtext{$email}.="$host:\n";
					}
				}
				$text = "UP $$watch[2]\n";
				$delta = TRUE;
			} elsif (($cur eq '-') && ($last eq '+')) {
				unless (defined $shown{$host}) {
					$shown{$host} = TRUE;
					foreach $email(keys %$alert) {
						$curtext{$email}.="$host:\n";
					}
				}
				$text = "DOWN $$watch[2]\n";
				$global_delta = TRUE;
				$delta = TRUE;
			}
			if ($delta == TRUE) {
				foreach $email(keys %$alert) {
					if (($$maxlen{$email} > 0) && (length($curtext{$email}) > $$maxlen{$email})) {
						SendMail($$alert{$email}, $curtext{$email});
						$curtext{$email}="$host:\n$text";
					} else {
						$curtext{$email}.=$text;
					}
				}
			}
			$delta = FALSE;
		}
	}
	if ($global_delta == TRUE) {
		foreach $email(keys %$alert) {
			SendMail($$alert{$email}, $curtext{$email});
		}
	}
}

sub SendMail {
	my ($email, $text) = @_;
	my $subject = 'ISpy status';
	DEBUG("sending email");
	DEBUG("- S -----------------------------------------");
	DEBUG("TO: $email");
	DEBUG("SUBEJCT: $subject");
	DEBUG("BODY:\n$text");
	DEBUG("- E -----------------------------------------\n");
	open MAIL, "|mail $email -s \"$subject\"";
	print MAIL $text;
	close MAIL;
}
