#!/usr/freeware/bin/perl  

# perldoc fw_securitycam.pl or scroll to bottom

my $VERSION = "1.0" ;

my $host = `hostname` ; chomp ($host) ;
my $date = localtime;
my $appname = ($0 =~ m/\/([0-9A-Za-z_.-]+)$/g)[-1];

##
# SET THESE AS NECESSARY!!!!
# I run this script as a non-root user, so ensure you set-up $sl_dir as
# writable as your user. 

#
# mailer settings
# 
my $to   = "rob\@dsvr.net" ;
my $msg  = "Security Camera Alert $host $date\n\n" ;
my $subj = "Security Camera Alert $host $date" ;

#
# securitylite settings, -c and -p can be passed from the command line (see -h)
#
my $securitylite 	= "/usr/freeware/bin/securitylite" ;
my $sl_dir  		= "/mnt/more/security/" ; #must have trailing slash
my $sl_log  		= ">> $ENV{HOME}/securitylitelog" ;
my $sl_change		= "-c 10" ;
my $sl_pixel		= "-p 25" ;
my $sl_extraopts    	= "-v 1 -n -d $sl_dir $sl_log" ;

my $dumpster = "$ENV{HOME}/dumpster" ;

my $hup_time  = 3600 ; # default 1 hour (ish)
my $exec_time = 12 ;   # iterations of main loop - ie total runtime 12 * 3600secs

my $extra_tests = 1 ; # set to zero to stop performing extra tests, which 
		      # might be sensible if you think they wont complete 
		      # in less then 1 second, or you think df is a bad thing
		     
my $auto_cleanup = 1 ; 	# automatically purge old security files (you may want 
			# to manage this yourself, change to 0)
			
my $auto_cleanup_days = 2 ; 	# files this old are moved to dumpster, if
				# $auto_cleanup = 1

my $quiet = 0 ; # see -q

my $sendmail = "/usr/lib/sendmail -oi -t -f $to" ; # smtp relays worth their 
						   # salt check From header 
						   # is resolvable which it 
						   # might not be if your 
						   # machine is behind a router
						   # so specify -f
$| = 1; # autoflush on

##
# Parse options 
# with the absence of Getopt::Long we do it the 
# manual way - probably unsafe, but this isn't
# a 100% robust script
# 
if ( @ARGV ) {

  my %opts ;
  my $prior_opt ;
  foreach ( @ARGV ) {
	if ( /^-/ ) {
		$opts{$_} ++ ; # simply set to something
		$prior_opt = $_; 
	} else {
		$opts{$prior_opt} = $_ ; 
	}
  }
  if ( exists ($opts{-q}) ) {
            $quiet ++ ; 
  }
	      
  if ( exists ($opts{-h}) ) {
	usage () ;
  }
  if ( exists ($opts{-p}) ) {
 	$sl_pixel = "-p $opts{-p}" ;
  } 
  if ( exists ($opts{-k}) ) {
 	killmyself() ; # kill that errant process
	exit 0 ;
  } 
  if ( exists ($opts{-c}) ) {
	$sl_change = "-c $opts{-c}" ;
  }
  if ( exists ($opts{-i}) ) {
	$exec_time = $opts{-i} ;
  } 
  if ( exists ($opts{-s}) ) {
 	$hup_time = $opts{-s} ;
  }
}

killmyself() ; # autoclean up prior running processes ... saves addition of a -k for crons

print ("$appname security monitor ver $VERSION starting up ...\n") ;

$securitylite .= " $sl_change $sl_pixel $sl_extraopts" ;

printf ("\n [ exec: $securitylite ]\n [ time: $exec_time iterations of $hup_time seconds; shutdown in %.2f hours ]\n", (($exec_time * $hup_time) / (60 * 60))) unless $quiet ;


for ( $exec_i = 0 ; $exec_i < $exec_time ; $exec_i ++ ) { 

  my $pid ;

  if ( $pid = fork ) {
    my $then ;
    my $now ;
  
    my $sent_mail = 0 ; # send once per iteration

    for ( $i = 0 ; $i < $hup_time ; $i ++ ) {
    
      my %warn ;
    
      if ( $sent_mail == 0 ) {

	# if already sent email on this run, don't
	# bother checking again (otherwise we'd be spewing
        # emails 
 
        ( $then, $now ) = watch_files ( $then, $now, $sl_dir ) ;

        foreach my $f ( keys %$now ) {
          if ( ( $now->{$f} ne $then->{$f} ) && ( $then->{$f} ne "" ) ) {
            $warn{$f} ++
          }
        }
    

        if ( %warn ) {
	
          $send_msg = $msg ;
	  
          foreach my $f ( keys %warn ) {
            $send_msg .= "CHANGED: $warn{$f} $then->{$f} to $now->{$f}\n"  ;
            $send_msg .= "/usr/freeware/bin/securityviewer $f" ;
          }
	  send_mail ( $sendmail, $to, $subj, $send_msg ) ;
	  $sent_mail ++ ;
        }

      }
      sleep( 1 ) ; 	# regiment checking to once a second
      			# so we don't thrash the system
      			
  
    } 
  
    print (" [ child: stopping securitylite in child $exec_i ]\n" ) unless $quiet ;
    kill 9, $pid ; # kill $securitylite
    waitpid ( $pid, 0 ) ;
    
    # perform some other nice tests, before starting again
    
    other_nice_tests ( $sendmail, $to, $subj, $sl_dir ) unless ! $extra_tests;  
    auto_cleanup ( $sendmail, $to, $subj, $sl_dir, $dumpster, $auto_cleanup_days ) unless ! $auto_cleanup ;

  } else {
	die "cannot fork: $!" unless defined $pid ;
	print (" [ child: starting securitylite in child $exec_i ]\n" ) unless $quiet ;
	exec ($securitylite) ;
  }

}
print ("\n... $appname shutting down\n") ;

sub watch_files { 

  my $then 	= shift ;
  my $now  	= shift ;
  my $sl_dir  	= shift ;

  # find files in $sl_dir and stat them, building two
  # hashes which can be compared

  my @watch = `find $sl_dir -type f` ;
  foreach my $f ( @watch ) {
    chomp ($f) ;

    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
         $atime,$mtime,$ctime,$blksize,$blocks)
             = stat($f);

    $then->{$f} = $now->{$f} ; 
    $now->{$f} = $size ;

  }
  
  return ( $then, $now ) ;
}


sub send_mail {

  my $sendmail = shift ;
  my $to       = shift ;
  my $subj     = shift ;
  my $msg      = shift ;

  # simple mail sender, using sendmail

  print ("\t [ sending mail $subj to $to ]\n" ) ;
  open( SENDMAIL, "| $sendmail" );
  print SENDMAIL "Subject: $subj\n";
  print SENDMAIL "To: $to\n";
  print SENDMAIL "\n$msg\n";
  close(SENDMAIL);
  
}

sub other_nice_tests {

  my $sendmail 	= shift ;
  my $to 	= shift ;
  my $subj 	= shift ;
  my $sl_dir 	= shift ;
  
  # do clever tests, such as df to ensure we have free space
  
  my $msg = "" ;
  my $min_usage = 85 ; # percent
 
  my $cmd = "df $sl_dir" ;
  
  my @df = `$cmd` ;
  
#  print ("\tops:\n   checking $cmd\n") ;
  
  foreach my $df_line ( @df ) {
  	chomp ( $df_line ) ;
	
	my @df_split = split ( /\s+/, $df_line ) ;
	# field 5 is percent
	# field 6 is path
	if ( $sl_dir =~ /$df_split[6]/ ) {
		if ( $df_split[5] > $min_usage ) {
		  $msg .= "WARNING: found mount point $df_split[0] is $df_split[5] % full" ;
		}
	}
  }

  if ( $msg ne "" ) {
	send_mail ( $sendmail, $to, "WARN: ".$subj, $msg ) ;
  }

}
sub auto_cleanup {

  my $sendmail 	= shift ;
  my $to 	= shift ;
  my $subj 	= shift ;
  my $sl_dir 	= shift ;
  my $dumpster 	= shift ;
  my $days	= shift ;
  

  # automatic cleanup of old security files.
  # choose files older than specified time 
  # and move them to Dumpster (or remove)

  # With the absence of Date::Manip we can use mtime
  # in find

  my $msg  = "" ;
  
  my $cmd  = "find $sl_dir -type f -mtime +$days" ;
 
  my @files = `$cmd` ;
  if ( @files > 0 ) {
  	print (" [ autocleanup: ... ") unless $quiet ;
	  
	foreach my $f ( @files ) {
	  print ( "\n\tcleaning up $f" ) unless $quiet ;
	  $msg .= "CLEANING file $f\n" ;
	  chomp ( $f ) ;

	  my @cmd_args = ("mv", $f, $dumpster) ;
          system(@cmd_args) == 0 or $msg .= "FAILED @cmd_args failed: $? $!\n" ;
	}
	
	print (" ... done ]\n") unless $quiet ;
	
	$msg .= "\nCheck the dumpster out\n" ;

    	send_mail ( $sendmail, $to, "CLEANUP: $subj", $msg ) ;
  }

}

sub usage {

  # Getopt::Long would be sooo useful here

  warn <<USAGE;
Usage: $appname [options]
options:
	-h 		this help
	-p num		how much change before pixel is counted as changed
	-c num		how many pixels must change before we write frame
	-i num		how many iterations of -h (make -1 for infinite)
	-s num 		hup-time default 3600 seconds. After hup-time we kill 
			and restart securitylite
	-k 		kill myself (useful for seemless cron action)
	-q		quiet - little output

you can perldoc me too.

examples:
	run once, 12 iterations of 1 hour (3600 seconds)
	./$appname -p 25 -c 40 -i 12 -s 3600

	run once from init script, with adjustable sensitivity
	./$appname -p 50 -c 30 -i 8 -s 3600 ; ./$appname -c 10 -p 25 -i 16 -s 3600

	run it from a cron every day, all day 
	(first, low sensitivity 4 x 2 hours, second higher sensitivity 16 x 1 hour) :

	14 08 * * * ./fw_securitycam.pl -p 50 -c 30 -i 4 -s 7200 ; ./fw_securitycam.pl -p 20 -c 10 -i 16 -s 3600 ;

USAGE

  exit 0 ;

}


sub killmyself {

  # crap death - yes yes i know, turns ur stomach, right.

 # print ("i am process $$, so make sure I don't kill myself...\n\n") ;

  my $found = 0 ;

  my @ps = `ps -o pid -o args -e | grep $0` ; #perl | grep $appname` ;
  foreach my $ps_line ( @ps ) {
	chomp ( $ps_line ) ;
	my @ps_split  = split ( /\s+/, $ps_line ) ;
#	print ("looking at process $ps_split[1] $ps_split[2] ... ") ;

#	this is a bugger - don't kill self, don't kill that grep $0 above and don't kill the cron /bin/sh ....
	if ( ($ps_split[1] != $$) && ($ps_split[2] !~ /^grep/) && ($ps_split[2] !~ /^\/bin\/sh/ ) ) {
#		print ("looking at process $ps_split[1] $ps_split[2] ... \n") ;
		print ("killing $ps_split[1] .. ") ;
		kill 15, $ps_split[1] or die "unable to kill process $ps_split[1]" ;
		$found ++ ;
	} else {
		#print ("skip, that's me\n") ;
	}
  }
  if ( $found > 0 ) {
  
    print ("killing securitylite ... ") ;
    `killall securitylite` ; # sick, because i duno enough about sig handling childs
  
    print ("cleaned up from previous run\n\n") ;

  }
 
}

=head1 NAME

fw_securitycam.pl  - A securitylite monitoring script

=head1 SYNOPSYS

fw_securitycam.pl [-h] [-p] [-c] [-i] [-s] [-k] [-q]

=head1 DESCRIPTION

A simple script to invoke securitylite, monitor it's output directory
and email you when something happens.

./fw_securitycam.pl -h

The script passes -p and -c options directly into securitylite to manage
sensitivity - see securitylite -h for further info. -s and -i options 
dictate how often the securitylite system is tested for and restarted. 
We restart periodically to manage the output files in cases where lots 
of movement causes large files.

YMMV adjust the var's to give the most acceptable results.

I have found running it from cron to be best, giving full cover with preprogrammed,
variable sensitivity. See the examples section for more information.

As an update, the script will now detect and kill a previous run of itself.

Why:

I wrote this script so that my Octane can watch the back of my house while
I'm at work.

=head1 TODO 

(if ever)
More real-world testing/feedback
Fix killing child process when -k kills parent 

=head1 KNOWN ISSUES 

The 'time' features are deliberately loose insofar as the main looping
should be 1 second long on anything but the slowest box (untested). This is
because we sleep for 1 second during normal looping, but then adhoc checks and cleanups
such as df and moving old files to the dumpster could take an unknown amount
of time - especially if they are large files. It's for this reason we
kill previous runs when we start up, to avoid clashing.

There are probably unsafe var usage but this isn't a script for an unsafe
multi user environment.

There are probably some cases where errors aren't graciously trapped, caught
or reported.

The -k option is awful.

Rather than spew emails out everytime movement was detected, email only
once per iteration. Cleanup ops and potential emails are still once per sec

=head1 REQUIRES 

fw_perl
fw_securitylite

working sendmail - see configmail setup

=head1 EXAMPLES

run once, 12 iterations of 1 hour (3600 seconds) with average sensitivity

./fw_securitycam.pl -p 25 -c 40 -i 12 -s 3600

run once for several hours from init script with adjustable sensitivity

./fw_securitycam.pl -p 50 -c 30 -i 8 -s 3600 ; ./fw_securitycam.pl -c 10 -p 25 -i 16 -s 3600

run it from a cron every day, all day (first, low sensitivity 4 x 2 hours, second higher sensitivity 16 x 1 hour) :

14 08 * * * ./fw_securitycam.pl -p 50 -c 30 -i 4 -s 7200 ; ./fw_securitycam.pl -p 20 -c 10 -i 16 -s 3600 ;

=head1 AUTHOR

Rob Fielding (F<rob@dsvr.net>)

=head1 COPYRIGHT

Copyright (C) 2003  Rob Fielding
 
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.

=head1 VERSION

1.0

=cut


1;

