#!/usr/local/bin/perl
#
# $Id: LogActiveX.pl,v 1.3 2005/01/20 01:02:59 rdilley Exp $
#
# author: ron dilley
#
# desc: this perl script looks for Active-X content
#
# Copyright (C) 2002  Ron A. Dilley
#
# 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; version 2
# of the License.
#
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# ron.dilley@uberadmin.com
#
############################################################################

#
# modules
#
use Getopt::Std;
use Sys::Syslog;
use FileHandle;
use POSIX;

#
# pragmas
#
use strict;

#
# set environment
#
$ENV{PATH} = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/ucb";

#
# turn on autoflush
#
select STDERR; $| = 1;
select STDOUT; $| = 1;

#
# defines
#
$::TRUE = 1;
$::FALSE = 0;
$::FAILED = -1;

$::VERSION = '$Id: LogActiveX.pl,v 1.3 2005/01/20 01:02:59 rdilley Exp $';
$::PROGNAME = "LogActiveX.pl";

%::Config = ();
$::Config{'debug'} = $::FALSE;
$::Config{'daemon'} = $::FALSE;
$::Config{'tcpflow'} = "/opt/tcpflow/current/bin/tcpflow -c -i hme0 tcp port 80 or tcp port 5150";
#$::Config{'tcpflow'} = "/apps/gnu/tcpflow/current/bin/tcpflow -c -i eth1 tcp port 80 or tcp port 5150";
$::Config{'session_timeout'} = 300;
$::Config{'time_sync'} = 5; # keep $::Config{'now'} updated to minimize calls to time()
$::Config{'utc_now'} = time();
$::Config{'log_file'} = "/var/tmp/logactivex.log";

%::sessions = ();

#
# main routine
#
if ( &main() != $::TRUE ) {
  exit( 1 );
}

exit( 0 );

############################################################################
#
# sub-routines
#

#
# main routine
#
sub main {
  my $arg;
  my $child_pid;

  #
  # display script banner
  #
  &show_banner();

  #
  # check for enough rights
  #
  if ( ! &is_root() ) {
    print "$0 must be run as root\n";
    return $::FAILED;
  }

  #
  # parse command-line
  #
  &parse_command_line();

  if ( $::Config{'daemon'} ) {
    #
    # run script as daemon
    #
    print STDERR "$::PROGNAME v$::VERSION starting in daemon mode\n";

    #
    # open syslog
    #
    openlog( $::PROGNAME, 'cons,pid', 'daemon' );

    &display( 'info', "Daemon startup v$::VERSION" );

    if ( getppid() == 1 ) {
      if ( $::Config{'debug'} >= 2 ) {
	&display( 'debug', "Why become a daemon, parent is init" );
      }
    } else {
      #
      # ignore the terminal stop signals
      #
      $SIG{TTOU} = 'IGNORE';
      $SIG{TTIN} = 'IGNORE';
      $SIG{TSTP} = 'IGNORE';

      if ( ( $child_pid = fork() ) < 0 ) {
	&display( 'err', "Unable to fork first child" );
	return $::FAILED;
      } elsif ( $child_pid > 0 ) {
	# this is the parent
	exit( 0 );
      }

      ###############################################################################
      #
      # this is the first child
      #

      if ( setpgrp() == $::FAILED ) {
	if ( $::Config{'debug'} ) {
	  &display( 'debug', "Unable to switch to process group leader" );
	}
	return $::FAILED;
      }

      #
      # make second child imune to process group leader death
      #
      $SIG{HUP} = 'IGNORE';

      if ( ( $child_pid = fork() ) < 0 ) {
	if ( $::Config{'debug'} ) {
	  &display( 'debug', "Unable to fork second child" );

	  return $::FAILED;
	}
      } elsif ( $child_pid > 0 ) {
	# this is the first child
	exit( 0 );
      }

      ###############################################################################
      #
      # this is the second child
      #

      #
      # move pwd to root to make sure we are not on a mounted fs
      #
      chdir( "/" );

      #
      # clear any inherited file mode creation mask
      #
      umask( 0 );
    }
  }

  #
  # defined pid file now, just in case we are running in daemon mode (several pid's have come and gone already)
  #
  if ( -d "/var/run" ) {
    $::Config{'pid_file'} = "/var/run/logactivex.$$.pid";
  } else {
    $::Config{'pid_file'} = "/var/tmp/logactivex.$$.pid";
  }

  #
  # note pid of parent (for external script automation)
  #
  if ( ! -l $::Config{'pid_file'} ) {
    if ( ! defined open( PID, "> $::Config{'pid_file'}" ) ) {
      &display( 'err', "Unable to create [$::Config{'pid_file'}]" );
      return $::FAILED;
    }
    if ( -l $::Config{'pid_file'} ) {
      &display( 'err', "Possible race condition after openning [$::Config{'pid_file'}]" );
      return $::FAILED;
    }
    # make file rw only for owner
    chmod( 0600, $::Config{'pid_file'} );
  } else {
    &display( 'err', "Possible race condition prior to openning [$::Config{'pid_file'}]" );
    return $::FAILED;
  }
  print PID "$$\n";
  # close pid file
  close( PID );

  #
  # get to work
  #

  # turn on our signal handlers for gracefull shutdown and log rotation
  &set_signal_handlers( $::TRUE );

  # start the alarm clock
  alarm $::Config{'time_sync'};

  # start watching the wire
  &watch_flow( @::ARGV );

  # done
  return $::TRUE;
}

#
# display banner info
#
sub show_banner {
  print "$::PROGNAME v$::VERSION\n";
  print "By: Ron Dilley\n";
  print "\n";
  print "$::PROGNAME comes with ABSOLUTELY NO WARRANTY.\n";
  print "This is free software, and you are welcome\n";
  print "to redistribute it under certain conditions;\n";
  print "See the GNU General Public License for details.\n";
  print "\n";

  return $::TRUE;
}

#
# display help info
#
sub show_help {
  print "Syntax:\n";
  print "\n";
  print "$::PROGNAME [options] [tcpflow regex]]\n";
  print "\n";
  print "-d {0-9}   Display debug information during program run\n";
  print "-b         Run in daemon mode\n";
  print "-f         Logfile name\n";
  print "\n";

  return $::TRUE;
}

#
# parse command-line arguments
#
sub parse_command_line {
  no strict 'vars';

  if ( getopts( 'd:bf:' ) == $::FALSE ) {
    &show_help();
    return $::FAILED;
  }
  if ( defined $opt_d ) {
    if ( $opt_d > 0 ) {
      # set debug mode
      $::Config{'debug'} = $opt_d;
    }
  }
  if ( defined $opt_f ) {
    if ( length( $opt_f ) > 0 ) {
      # override logfile
      $::Config{'logfile'} = $opt_f;
    }
  }
  if ( defined $opt_b ) {
    # set background mode
    $::Config{'daemon'} = $opt_b;
  }
  return $::TRUE;
}

#
# follow the flow
#
sub watch_flow {
  my ( @extra_args ) = @_;
  my $line;
  my $source_addr;
  my $source_port;
  my $dest_addr;
  my $dest_port;
  my $flow_fh = new FileHandle;
  my $log_fh = new FileHandle;
  my $tmp_read_buf = "";
  my $read_buf;
  my $read_count;
  my $pipe_blksize;
  my $idle;
  my $get_string;
  my $tmp_session_id;
  my $current_session_id;
  my $data;
  my $key;
  my $string;

  if ( ! defined open( $flow_fh, "$::Config{'tcpflow'} @extra_args |" ) ) {
    &display( 'err', "Unable to start [$::Config{'tcpflow'}] with [@extra_args]" );
    return $::FAILED;
  }

  # force autoflush
  select $flow_fh; $| = 1;
  select STDOUT;

  # get io block sizes
  $pipe_blksize = ( stat( $flow_fh ) )[11];

  # disable blocking
  if ( ! defined fcntl( $flow_fh, F_SETFL(), O_NONBLOCK() ) ) {
    &display( 'err', "Unable to disable blocking on handle" );
    return $::FAILED;
  }

  # toggle log rotate to get the logs opened the first time
  $::Config{'new_log_file'} = $::TRUE;

  while( ! $::Config{'terminate'} ) {
    #
    # check for new log request
    #
    if ( $::Config{'new_log_file'} ) {
      &display( 'info', "Log rotation restart requested" );
      # open automatically closes the handle (if it was already open)
      if ( ! -l $::Config{'log_file'} ) {
	if ( ! defined open( $log_fh, ">> $::Config{'log_file'}" ) ) {
	  &display( 'err', "Unable to create [$::Config{'log_file'}]" );
	  $::Config{'terminate'} = $::TRUE;
	}
	if ( -l $::Config{'log_file'} ) {
	  &display( 'err', "Possible race condition after openning [$::Config{'log_file'}]" );
	  $::Config{'terminate'} = $::TRUE;
	}
	# change file perms
	chmod( 0640, $::Config{'log_file'} );
      } else {
	&display( 'err', "Possible race condition while openning [$::Config{'log_file'}]" );
	$::Config{'terminate'} = $::TRUE;
      }
      $::Config{'new_log_file'} = $::FALSE;
      # force autoflush
      select $log_fh; $| = 1;
      select STDOUT;
    }

    $read_buf = "";
    if ( defined ( $read_count = sysread( $flow_fh, $read_buf, $pipe_blksize ) ) ) {
      if ( $read_count > 0 ) {
	$idle = $::FALSE;
	if ( $::Config{'debug'} >= 6 ) {
	  &display( 'debug', '[%d] bytes read from pipe', $read_count );
	}
	# if there was data left from the last read, prepend it
	if ( length( $tmp_read_buf ) > 0 ) {
	  $read_buf = sprintf( "%s%s", $tmp_read_buf, $read_buf );
	  $tmp_read_buf = "";
	}
	# chop up buffer into lines and process them
	while( $read_buf =~ s/^(.*\n)// ) {
	  $line = $1;
	  chomp $line;
	  # 138.133.010.084.05150-138.133.008.058.52660:
	  if ( $line =~ m/^(\d{3,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\.(\d{1,5}).*(\d{3,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\.(\d{1,5})\:.*GET (.*) HTTP\/1\.\d.*/ ) {
	    #
	    # start of new session
	    #
	    $source_addr = $1;
	    $source_port = $2;
	    $dest_addr = $3;
	    $dest_port = $4;
	    $get_string = $5;
	    if ( $::Config{'debug'} >= 3 ) {
	      &display( 'debug', "Flow data [$source_addr $source_port] to [$dest_addr $dest_port] get [$get_string]" );
	    }
	    # create a session record
	    my %tmp_session = ();
	    $tmp_session{'mod_time'} = $::Config{'utc_now'};
	    $tmp_session{'client_addr'} = $source_addr;
	    $tmp_session{'client_port'} = $source_port;
	    $tmp_session{'server_addr'} = $dest_addr;
	    $tmp_session{'server_port'} = $dest_port;
	    $tmp_session{'get_request'} = $get_string;
	    $tmp_session{'leftover_data'} = "";
	    $tmp_session_id = "$source_addr.$source_port-$dest_addr.$dest_port";
	    $::sessions{$tmp_session_id} = \%tmp_session;
	    if ( $::Config{'debug'} >= 4 ) {
	      &display( 'debug', "[%d] Active sessions", scalar keys( %::sessions ) );
	    }
	    $current_session_id = $tmp_session_id;
	  } elsif ( $line =~ m/^(\d{3,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\.(\d{1,5}).*(\d{3,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\.(\d{1,5})\:(.*)$/ ) {
	    #
	    # continuation of session
	    #
	    $source_addr = $1;
	    $source_port = $2;
	    $dest_addr = $3;
	    $dest_port = $4;
	    $data = $5;
	    #Content-Length: 43
	    #Content-Type: image/gif
	    $tmp_session_id = "$source_addr.$source_port-$dest_addr.$dest_port";
	    if ( exists $::sessions{$tmp_session_id} ) {
	      if ( $::Config{'debug'} >= 5 ) {
		&display( 'debug', "$tmp_session_id request continuation" );
	      }
	      $current_session_id = $tmp_session_id;
	      # update mod time
	      $::sessions{$current_session_id}->{'mod_time'} = $::Config{'utc_now'};
	      $::sessions{$current_session_id}->{'leftover_data'} = $data;
	    } else {
	      $tmp_session_id = "$dest_addr.$dest_port-$source_addr.$source_port";
	      if ( exists $::sessions{$tmp_session_id} ) {
		if ( $::Config{'debug'} >= 5 ) {
		  &display( 'debug', "$tmp_session_id reply continuation" );
		}
		$current_session_id = $tmp_session_id;
		# update mod time
		$::sessions{$current_session_id}->{'mod_time'} = $::Config{'utc_now'};
		$::sessions{$current_session_id}->{'leftover_data'} = $data;
	      } else {
		# unaccounted for data
	      }
	    }
	  } else {
	    #
	    # data in the current session
	    #
	    if ( $::Config{'debug'} >= 8 ) {
	      &display( 'debug', "$current_session_id data [%s bytes]", length( $line ) );
	    }
	    if ( exists $::sessions{$current_session_id} ) {
	      if ( exists $::sessions{$current_session_id}->{'leftover_data'} ) {
		if ( length( $::sessions{$current_session_id}->{'leftover_data'} ) > 0 ) {
		  # prepend old data to current line
		  $line = sprintf( "%s%s", $::sessions{$current_session_id}->{'leftover_data'}, $line );
		  $::sessions{$current_session_id}->{'leftover_data'} = "";
		}
	      }
	    }
	    $line =~ s/[\r|\n]//g;
	    $line =~ tr/a-z/A-Z/;
	    # look for embed tags
	    if ( $line =~ m/.*<EMBED.*/ ) {
	      if ( $::Config{'debug'} >= 3 ) {
		&display( 'debug', "Embed tag found in [$current_session_id]" );
	      }
	      print $log_fh "$current_session_id EMBED $::sessions{$current_session_id}->{'get_request'}\n";
	    }
	    # look for object tags
	    if ( $line =~ m/.*<OBJECT.*/ ) {
	      if ( $::Config{'debug'} >= 3 ) {
		&display( 'debug', "Object tag found in [$current_session_id]" );
	      }
	      print $log_fh "$current_session_id OBJECT $::sessions{$current_session_id}->{'get_request'}\n";
	    }

	    # store the last of the line
	    $::sessions{$current_session_id}->{'leftover_data'} = substr( $line, -6 );
	  }
	}
	# check for partial line in read_buf
	if ( length( $read_buf ) > 0 ) {
	  # data left in buffer, save it for the next pass
	  $tmp_read_buf = $read_buf;
	  $read_buf = "";
	}
      }
    } else {
      if ( $! == EAGAIN() ) {
	# no problem, keep going
	# no data read, look for old sessions
	foreach $key ( keys( %::sessions ) ) {
	  if ( exists $::sessions{$key}->{'mod_time'} ) {
	    if ( ( $::Config{'utc_now'} - $::sessions{$key}->{'mod_time'} ) > $::Config{'session_timeout'} ) {
	      if ( $::Config{'debug'} >= 5 ) {
		&display( 'debug', "Session timed out [$key]" );
	      }
	      delete $::sessions{$key};
	      if ( $::Config{'debug'} >= 4 ) {
		&display( 'debug', "[%d] Active sessions", scalar keys( %::sessions ) );
	      }
	    }
	  }
	}
	sleep( 1 );
      } else {
	# problem, time to break out
	$::Config{'terminate'} = $::TRUE;
	&display( 'info', "Problem while reading from pipe" );
      }
    }
  }

  close( $log_fh );
  close( $flow_fh );
  return $::FAILED;
}

#
# set signal handlers
#
sub set_signal_handlers {
  my ( $command ) = @_;

  if ( $command == $::TRUE ) {
    $SIG{HUP} = 'signal_handler';
    $SIG{TERM} = 'signal_handler';
    $SIG{QUIT} = 'signal_handler';
    if ( $::Config{'daemon'} ) {
      $SIG{INT} = 'IGNORE';
    } else {
      $SIG{INT} = 'signal_handler';
    }
    # for keeping current time
    $SIG{ALRM} = 'alarm_signal_handler';
    if ( $::Config{'debug'} >= 8 ) {
      &display( 'debug', "Customizing signal handlers" );
    }
  } else {
    $SIG{HUP} = 'DEFAULT';
    $SIG{TERM} = 'DEFAULT';
    $SIG{QUIT} = 'DEFAULT';
    $SIG{INT} = 'DEFAULT';
    $SIG{ALRM} = 'DEFAULT';
    if ( $::Config{'debug'} >= 8 ) {
      &display( 'debug', "Returning signal handlers to default" );
    }
  }
  return $::TRUE;
}

#
# signal handler
#
sub signal_handler {
  my( $sig ) = @_;

  if ( $::Config{'debug'} >= 6 ) {
    &display( 'info', "Caught a SIG$sig" );
  }
  # disable handlers
  &set_signal_handlers( $::FALSE );
  if ( $sig eq 'HUP' ) {
    $::Config{'new_log_file'} = $::TRUE;
  } elsif ( $sig eq 'TERM' || $sig eq 'QUIT' || $sig eq 'INT' ) {
    $::Config{'terminate'} = $::TRUE;
  }
  # enable handlers
  &set_signal_handlers( $::TRUE );
  return $::FALSE;
}

#
# alarm signal handler
#
sub alarm_signal_handler {
  my( $sig ) = @_;

  if ( $::Config{'debug'} >= 6 ) {
    &display( 'info', "Caught a SIG$sig" );
  }
  if ( $sig eq 'ALRM' ) {
    $::Config{'utc_now'} = time();
    alarm $::Config{'time_sync'};
  }
  return $::FALSE;
}

#
# send output to stdout or syslog
#
sub display {
  my ( $level, $formatting, @arguments ) = @_;

  if ( $::Config{'daemon'} ) {
    syslog( $level, $formatting, @arguments );
  } else {
    $level =~ tr/a-z/A-Z/;
    printf( "%s[%d] - " . $formatting . "\n", $level, $$, @arguments );
  }
  return $::TRUE;
}

#
# test to see if script was run as root
#
sub is_root {
  my $username;
  if ( -f "/usr/bin/whoami" ) {
    chop( $username = `/usr/bin/whoami` );
  } else {
    chop( $username = `/usr/ucb/whoami` );
  }
  if ( $username eq "root" ) {
    return $::TRUE;
  }
  return $::FALSE;
}
