#!/usr/bin/perl
#! $Id: sudoscriptd,v 1.11 2002/04/03 19:55:35 howen Exp $
use strict;
use POSIX qw(mkfifo setsid tzname);
use POSIX qw(:sys_wait_h);

# Set up some date values;
POSIX::localtime(time);
my @zones=tzname();

# Become a daemon
my $pid=fork;
exit if $pid;
die "Couldn't fork $!" unless defined($pid);
die "Couldn't start new session $!" unless POSIX::setsid();

# Record our PID for those who would wish us ill 8)
my $rundir="/var/run/sudoscript";
open PID,">/var/run/sudoscriptd.pid";
print PID "$$\n";
close PID;

# Open the log
my $MAXLOGSIZE=1024*1024*2;
my $size=openlog();

# Create and open the FIFO that sudoshell will log to
my $fifo="$rundir/typescript";
mktypescript();

while (1){
  no strict qw(subs);
  sysopen(FIFO, $fifo,O_RONLY);
  use strict;
  while (<FIFO>){
    $_=datestamp()." $_";
    $size += length; # Base $size is set in openlog();
    print LOG;
    waitpid -1,&WNOHANG; # Reap any compressors out there
    if ($size > $MAXLOGSIZE){
      $size=0;
      openlog();
    }
  }
}

sub mktypescript {
  # Create the fifo directory if it doesn't exist
  if (! -d $rundir){
    mkdir $rundir, 0700 || die "Can't mkdir $rundir $!";
  }
  # Nuke any old FIFOs left (NB: This daemon is _single_threaded_!)
  # (Unlike sudoshell)
  if (-e $fifo){
    unlink $fifo;
  }
  mkfifo($fifo,0600) || die "Can't make fifo $fifo $!";
}
sub openlog{
  # (re)open the log file
  # Rotate the logs if larger than $MAXLOGSIZE
  my $log="/var/log/sudoscript";
  my $pid;
  my($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size);
  if (-e $log){
    ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size)=stat $log;
    $size=rotate_log() if ($size>$MAXLOGSIZE)
  }
  my $op = ">";
  $op.=">" if (-e $log);
  chmod 0600,$log;
  open LOG, "$op$log";
  my $foo=select LOG;
  $|=1; # Unbuffered
  select $foo;
  return $size;
}
sub rotate_log {
  # Rotate the log files
  my $logdir="/var/log";
  my $logbase="sudoscript";
  my $log="$logdir/$logbase";
  my $tlog="$log.$$";
  # Move the old log out of the way
  link $log, $tlog;
  unlink $log;
  # Fork a child to do the rotation/compression
  my ($pid)=fork;
  return $pid if $pid;
  # CHILD
  my $MAXLOGS=10;
  my ($currlog,$lastlog);
  for (my $i=$MAXLOGS-1;$i;$i--){
    $currlog="$logdir/$logbase.$i.gz";
    $lastlog="$logdir/$logbase.".($i+1).".gz";
    if (-e $currlog){
      unlink $lastlog;
      link $currlog,$lastlog;
    }
  }
  unlink "$logdir/$logbase.1.gz";

  my $cmd="gzip -c $tlog >$logdir/$logbase.1.gz";
  `$cmd`;
  chmod 0600,"$logdir/$logbase.1.gz";
  unlink $tlog;
  exit 0;
}
sub datestamp {
  # Sorta kinda syslog format
  my @monames=('Jan','Feb','Mar','Apr','May','Jun',
	       'Jul','Aug'.'Sep','Oct','Nov','Dec');
  my @wdnames=('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
  my ($s,$mi,$h,$d,$mo,$y,$wday,$yday,$isdst)=localtime();
  return sprintf "%3s %3s %02d %02d:%02d:%02d %3s %04d",
    $wdnames[$wday],$monames[$mo],$d,$h,$mi,$s,$zones[$isdst],$y+1900;
}

=pod

=head1 NAME

  sudoscriptd

=head1 SYNOPSIS

  sudoscriptd

=head1 DESCRIPTION

I<sudoscriptd> is a daemon for logging output from L<sudoshell(8)>.
Used with that script, it provides an audit trail for shells run under
sudo.

=head1 README

I<sudoscriptd>
creates a named pipe (FIFO) in a spool area, and hangs around waiting
for someone to write to it. When output is received, it is timestamped
and placed in a log file in /var/log. The size of the data received is
monitored. When the size of the log plus the size of the data exceed
2MB, the log is rotated and gzipped in a subprocess. At most 10 logs
are kept hanging around. Multiple processes can write to the fifo at 
the same time. The output will get a little jumbled, but timestamps may
help you sort things out.

=head1 FILES

The fifo is /var/run/sudocript/typescript. It is owned by root and
mode 600. The log files are named /var/log/sudoscript(.extension if any).
Sudoscriptd stores its PID in /var/run/sudoscriptd.pid.

=head1 BUGS

If the log file is greater than 2MB in size when sudoscript runs, it will
fork a process to compress the log, but will not I<waitpid> until output
is first detected on the fifo. This will result in a zombie process in
the system until the fifo is used by L<sudoshell(8)>, at which time
the child process will be reaped.

The datestamp() routine is not locale aware and returns American
English values.

=head1 SEE ALSO

L<sudoshell>

=head1 LICENSE

L<sudoscriptd> may be distributed under the same terms as Perl itself.

=head1 PREREQUISITES

sudoshell - L<http://www.egbok.com/sudoscript/sudoshell> or on CPAN

sudo - L<http://www.courtesan.com/sudo/index.html>

=head1 OSNAMES

C<Solaris>

C<Linux>

C<freebsd>

C<openbsd>

=head1 SCRIPT CATEGORIES

UNIX/System_administration

=head1 CONTRIBUTORS

The following people offered helpful advice:

   Dan Rich       (drich@emplNOoyeeSPAMs.org)
   Alex Griffiths (dag@unifiedNOcomputingSPAM.com)
   Bruce Gray     (bruce.gray@aNOcSPAMm.org)

=head1 AUTHOR

Howard Owen (hbo@egbok.com)