#!/usr/bin/perl

################################################################################
#                                                                              #
# Version 2.15 by Nick Reinking, original code by Charlie Cook.                #
# See LICENSE for copyright information                                        #
#                                                                              #
# netsaint_statd [port]                                                        #
#                                                                              #
# netsaint_statd is a daemon which allows for a Netsaint host using            #
# scripts to get information suck as process count, users, disk usage, and     #
# load information.  This daemon does not process the information in anyway.   #
# It merely collects the info and hands it back to the calling script to do    #
# with as it pleases.                                                          #
#                                                                              #
# This script is designed in such a way as to allow for easy porting via the   #
# %commandlist hash.  Adding other checks should also be as easy as making a   #
# minor modification to the %commandlist hash for the wanted command.          #
#                                                                              #
# Function calls have been recently reworked.  This is to allow for *much*     #
# easier extensions of netsaint_statd.  When a client communicates to          #
# netsaint_statd, it sends one line, which contains a request for information  #
# on a particular resource, and arguments for that request.  The new           #
# netsaint_statd will call the function by the resource called, and pass it    #
# the arguments.  For example, client A connections to server B, and sends the #
# line "dog /with/one/tail".  netsaint_statd will then call the function dog() #
# and pass it the parameter /with/one/tail.  This should make adding extra     #
# functionality as easy adding a single function, and modifying an existing    #
# client script.  Note that you must enter these functions in the dispatch     #
# table below. If you need to call commands from the %commandlist for your     #
# functions, they are referenced by $commandlist{$os}{functionname}.  So, if   #
# you want the df command for your current machine, you can get it by          #
# accessing $commandlist{$os}{dfcommand}.  It's that easy.  Sending the client #
# information is a simple 'print Client information'.                          #
#                                                                              #
################################################################################
#
# modification history:
# 20030201	using "df -P" on Linux for long device names in df check
# 20030201	procs check initializes to 0 instead of -1 
# 		(Debian bugs #149742 and #149821 
# 		 thanks to Peter Palfrader <weasel@debian.org>)
# 20030201	better proc state regex (Debian bug #153958)
# 		(thanks to Rainer Clasen <bj@zuto.de>)
# 20030218	allow '.' to appear in command argument
# 20030405	change to SCO uname (now "SCO_SV")
# 		thanks to Torsten Warncke
# 20030530	security fix for named_proc/now using Perl's grep)
# 		(Debian bug #195326 - thanks to Paul Fenwick for the patch)
# 20040901	somehow we lost the -P on the df check for Linux
# 20040926	using /usr/bin/free instead of /proc/meminfo for swapcommand
#	(Debian bug #273566 - thanks to Craig Small)
################################################################################
# VERSION allows us to check netsaint_statd versions in large environments
# using the check_netsaint_statd.pl plugin 
my $VERSION = "2.15-9";

# Uncomment @allowed_hosts if you want to restrict access to only
# certain hosts.  Leaving it uncommented allows everybody to access
# netsaint_statd.  I recommend placing the IP of your netsaint
# server in here, for security reasons (not that my program has any
# security bugs).  :P

my @allowed_hosts; # = ("127.0.0.1","10.20.30.40","123.123.123.123");

my $os = os_uname();
$os = "IRIX" if $os =~ "IRIX64"; # IRIX can have two different unames.
# my $os = "NEXTSTEP";  # Uncomment this if you have a NeXT machine.

my %commandlist = (
	"HP-UX" => {
		dfcommand => "/bin/bdf -l",
		whocommand => "/bin/who -q | /usr/bin/grep \"#\"",
		proccommand => "/bin/ps -el",
		procstates => "SWRITZX",
		uptimecommand => "/bin/uptime",
	},
	"Linux" => {
		dfcommand => "/bin/df -Pk",
		whocommand => "/usr/bin/who -q | /bin/grep \"#\"",
		swapcommand => "/usr/bin/free | /bin/grep \"^Swap:\" | /usr/bin/awk '{ print (\$3/\$2)*100 }'",
		proccommand => "/bin/ps ax",
		procstates => "RSTDZ",
		uptimecommand => "/usr/bin/uptime",
	},
	"SunOS" =>	{
		dfcommand => "/bin/df -k",
		whocommand => "/bin/who -q | /usr/bin/grep \"#\"",
		swapcommand => "/usr/sbin/swap -s | /bin/tr -d -s -c [:digit:][:space:] | nawk '{print (\$3/(\$3+\$4))*100}'",
		proccommand => "ps -e -o \"pid tty s time comm\"",
		procstates => "SRZTO",
		uptimecommand => "/usr/bin/uptime",
	},
	"IRIX" => {
		dfcommand => "/bin/df -kP",
		whocommand => "/bin/who -q | /usr/bin/grep \"#\"",
		proccommand => "ps -e -o \"pid tty state time comm\"",
		procstates => "SRZTIXC0",
		uptimecommand => "/usr/bsd/uptime",
	},
  "OSF1" => {
		dfcommand => "/bin/df -k",
		whocommand => "/bin/who -q | /usr/bin/grep \"Total user\"",
		proccommand => "ps -e -o \"pid tty state time comm\"",
		procstates => "RVSITH",
		uptimecommand => "/bin/uptime",
	},
  "FreeBSD" => {
		dfcommand => "/bin/df -k",
		whocommand => "/usr/bin/who | /usr/bin/wc -l",
		proccommand => "/bin/ps ax",
		procstates => "RSTDIZ",
		uptimecommand => "/usr/bin/uptime",
	},
  "NEXTSTEP" => {
		dfcommand => "/bin/df",
		whocommand => "/bin/who | /usr/ucb/wc -l",
		proccommand => "/bin/ps -ax",
		procstates => "RSTDIZ",
		uptimecommand => "/usr/bin/uptime",
	},
  "BSD/OS" => {
		dfcommand => "/bin/df",
		whocommand => "/usr/bin/who | /usr/bin/wc -l",
		proccommand => "/bin/ps -ax",
		procstates => "RSTDIZ",
		uptimecommand => "/usr/bin/uptime",
	},
  "OpenBSD" => {
		dfcommand => "/bin/df -k",
		whocommand => "/usr/bin/who | /usr/bin/wc -l",
		proccommand => "/bin/ps -ax",
		procstates => "RIDLIS",
		uptimecommand => "/usr/bin/uptime",
	},
  "AIX" => {
		dfcommand => "/usr/bin/df -Ik",
		whocommand => "/usr/bin/who | /usr/bin/wc -l",
		swapcommand => "lsps -sl | grep -v Paging | awk '{print \$2}' | cut -f1 -d%",
		proccommand => "/usr/bin/ps x",
		procstates => "OAWISTZ",
		uptimecommand => "/usr/bin/uptime",
	},
  "NetBSD" => {
		dfcommand => "/usr/bin/df -k",
		whocommand => "/usr/bin/who | /usr/bin/wc -l",
		proccommand => "/bin/ps ax",
		procstates => "RSTDIZ",
		uptimecommand => "/usr/bin/uptime",
	},
  "UNIXWARE2" => {
		dfcommand => "/usr/ucb/df",
		whocommand => "/usr/bin/who -q | /bin/grep \"#\"",
		proccommand => "/usr/bin/ps -el | awk '{printf(\"%6d%9s%2s%5s %s\\n\",\$4,substr(\$0, 61, 8),\$2,substr(\$0,69,5),substr(\$0,75))}",
		procstates => "OSRIZTX",
		uptimecommand => "echo `/usr/bin/uptime`, load average: 0.00, `sar | awk '{oldidle=idle;idle=\$5} END {print 100-oldidle}'`,0.00",
	},
  "SCO_SV" => {
		dfcommand => "/bin/df -Bk",
		whocommand => "/bin/who -q | /usr/bin/grep \"#\"",
		proccommand => "ps -el -o \"pid tty s time args\"",
		procstates => "OSRIZTB",
		uptimecommand => "/usr/bin/uptime",
	}
); 

# Dispatch table for subroutines

my %subroutines =
	(
	"users"      => \&users,
	"disk"       => \&disk,
	"alldisks"   => \&alldisks,
	"uptime"     => \&uptime,
	"procs"      => \&procs,
	"named_proc" => \&named_proc,
	"version"    => \&version,
	"swap"       => \&swap,
	"filemodinterval"    => \&filemodinterval
	);

################################################################################
################################################################################

require 5.003;
BEGIN { $ENV{PATH} = '/bin:/usr/bin:/usr/local/bin:/usr/sbin' }
use POSIX;
use Socket;
use strict;

# Setup upgly global varialbes
my %restrictions;

# Forking into a new daemon...

my $pid = fork;
exit if $pid;
die "Couldn't fork: $!\n" unless defined($pid);
POSIX::setsid() || die "Cannot spawn new session id: $!\n";

# hack in support for PIDFILE by <tmancill@debian.org>
system("echo $$ >/var/run/netsaint_statd.pid");

# Verifying IP restriction information...

&verify_ip_list if @allowed_hosts;

# Extra information needed...

my $port = shift || 1040;
chomp($os);

# Setting up server for listening...
my $proto = getprotobyname('tcp');
socket(Server, PF_INET, SOCK_STREAM, $proto) || die "Can't create socket: $!\n";
setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, 1) || die "Can't setsockopt $!\n";
bind(Server, sockaddr_in($port, INADDR_ANY)) || die "Can't bind to socket: $!\n";
listen(Server,SOMAXCONN) || die "Can't listen to socket: $1";

# Infinite loop listening for client connections...

while (my $paddr = accept(Client,Server))
	{
	if (@allowed_hosts)
		{
		my ($cport, $packed_ip) = sockaddr_in($paddr);
		my $dotted_quad = inet_ntoa($packed_ip);
		unless ($restrictions{$dotted_quad})
			{
			send(Client,"Sorry, you ($dotted_quad) are not among the allowed hosts...\n",0);
			close(Client);
			next;
			}
		}
	my ($command,$arg_for_command,$input) = undef;
	next unless(defined($input = <Client>));
	if ($input =~ /^(\w*)\s?([\w\-\/\.]*)/)
		{
		$command = $1;
		$arg_for_command = $2 if $2;
 		$command =~ tr/A-Z/a-z/;
		}

# Call function by name (if in dispatch table)...

	if (exists $subroutines{$command})
		{
		my $rsub = $subroutines{$command};
		&$rsub($arg_for_command);
		close(Client);
		}
	else
		{
		send(Client,"Unknown command\n",0);
		close(Client);
		}
	}

sub verify_ip_list
################################################################################
# verify_ip_list scans the @allowed_hosts array, and double checks to make     #
# sure that you didn't put anything that isn't an IP address in there.         #
################################################################################
	{
	for (my $i=0;$i<=$#allowed_hosts;$i++)
		{
		if ($allowed_hosts[$i] =~ /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/)
			{
			$restrictions{$allowed_hosts[$i]} = 1;
			}
		else
			{
			print "Sorry, your allowed hosts list doesn't contain valid IPs.\n";
			exit(0); 
			}
		}
	}

sub os_uname
	{
	my $uname = ( -e '/usr/bin/uname' ) ? '/usr/bin/uname' : '/bin/uname';
	my $os = (`$uname 2>/dev/null`);
	chomp $os;
	return $os ? $os : undef;
	}

sub users
	{
	open(WHOOUT,"$commandlist{$os}{whocommand} |") || die;
	$_ = <WHOOUT>;
	$_ =~ /[users]*[=|:]?\s?(\d+)/;
	my $users = $1;
	print Client "$users ";
	close(WHOOUT);
	}

sub disk
	{
	my $arg = shift;
	my ($disk, $avail, $capper, $mountpt);

	open(DFOUTPUT,"$commandlist{$os}{dfcommand} |") || die;
	$_ = <DFOUTPUT>;
	DFCHECK: while($_ = <DFOUTPUT>)
		{   
		if (/^([\w\/\:_\.\-\=]*)\s*\d*\s*\d*\s*(\d*)\s*(\d*)\%\s*([\w\/\-]*)/)
			{
			$disk = $1;
			$avail = $2;
			$capper = $3;
			$mountpt = $4;
			last DFCHECK if ($disk eq "$arg");
			}
		}
		
	# assert:  it is possible to fall out of this loop 
	# with $disk set and still not have matched the 
	# device name passed as an arg <tmancill@debian.org>
	if (($disk eq "$arg") && ($disk && $mountpt)) 
		{
		$capper = 100 - $capper;
		print Client "$disk $avail $capper $mountpt ";  
  		} 
	else 
		{
		# the last filesystem didn't match either
		print Client "$arg not found";
		}
	
	($disk,$avail,$capper,$mountpt) = undef;
	close(DFOUTPUT);
	}

sub alldisks
	{
	my $disklisting;

	open(DFOUTPUT,"$commandlist{$os}{dfcommand} |") || die;
	$_ = <DFOUTPUT>;
	while($_ = <DFOUTPUT>)
		{
		if (/^[\w\/\:\.\-\=]*\s*\d*\s*\d*\s*\d*\s*(\d*)\%\s*([\w\/\-]*)/)
			{
			$disklisting .= "(".$2.",".$1.")";
			}
		}
	if ($disklisting)
		{
		print Client $disklisting;
		}
	else
		{
		print Client "no disks?";
		}
	close(DFOUTPUT);
	undef $disklisting;
	}
   
sub uptime
	{
	open(UPTIMEOUTPUT,"$commandlist{$os}{uptimecommand} |");
	$_ = <UPTIMEOUTPUT>;
	$_ =~ /up\s*(.*),\s*\d*\s*user[s]?,\s*load average[s]?:\s*[0-9\.]*,\s*([0-9\.]*),/;
	print Client "$1 - $2 ";
	close(UPTIMEOUTPUT);
	} 

sub procs
	{
	my $procs = 0;
	my $procflags = shift || $commandlist{$os}{procstates};
	open(PROCOUT, "$commandlist{$os}{proccommand} |") || die;
	$_ = <PROCOUT>;
	if ($procflags)
		{
		while (defined($_ = <PROCOUT>))
			{
			if ($_ =~ /\s+\w*([$procflags]+)\w*\s+\d*/)
				{
				$procs++;
				}
			}
		}
	else
		{
		while (<PROCOUT>)
			{
			$procs++;
			}
		}
	print Client "$procs $procflags ";
	($procs, $procflags) = undef;
	close(PROCOUT);
	}

sub named_proc {
	my $args = shift;
	open(PROCOUT, "$commandlist{$os}{proccommand} |") || die;
	@_ = <PROCOUT>;
	# prevent counting the named process check on the netsaint server
	@_ = grep { !/\/usr\/lib\/netsaint\/plugins\/check_named_proc.pl / } @_;
	@_ = grep { /$args/ } @_;
	print Client ++$#_;
	close(PROCOUT);
}
	
# added by <tmancill@debian.org>	
sub version 
        {
	print Client "$VERSION";
	}

# added by <tmancill@debian.org> 
sub swap
	{
	if (defined $commandlist{$os}{swapcommand}) 
		{
		print Client `$commandlist{$os}{swapcommand}`;
		} 
	else 
		{
		print Client "swapcommand not defined for $os";
		}
	}

# added by <tmancill@debian.org> 
# $stat[9] returns mtime in seconds since the epoch
sub filemodinterval
{
	my $filename = shift;
	my $currenttime = time;
	if (-e $filename) {
	   if (my @filestat = stat $filename) {
	      print Client ($currenttime - $filestat[9]);
	   } else {
	      print Client "pluginerror: unable to stat $filename - check perms";
	   }
        } else {
	   print Client "pluginerror: filename $filename not found";
	}
}

