# SyslogEntry: generic line in a syslog program.

package SyslogScan;

$VERSION = 0.30;
sub Version { $VERSION };


package SyslogScan::SyslogEntry;

use SyslogScan::ParseDate;

$VERSION = 0.30;
sub Version { $VERSION };

use SyslogScan::UnsupportedEntry;
use strict;

# to handle 'last message repeated n times' lines
my %gLastLineByHost;
my $gLineToRepeat;
my $gFinalMonth;
my $gFinalDay;
my $gFinalTime;
my $gRepeatCount = 0;

my %gTable = 
    (
# examples:
#     'cli'         =>   'SyslogScan::AnnexEntry',
#     'slip'        =>   'SyslogScan::AnnexEntry',
#     'telnet_cmd'  =>   'SyslogScan::AnnexEntry',
#     'ppp'         =>   'SyslogScan::AnnexEntry',
#     'rlogin_rdr'  =>   'SyslogScan::AnnexEntry',
     );

my $pIsSubclass = sub {
    my($superclass,$possibleSubclass) = @_;
    my(@superclassList);
    
    die "illegal subclass (has whitespace)" if
	$possibleSubclass =~ /\s/;
    @superclassList = eval '@' . "$possibleSubclass" . "::ISA";
    return 't' if (grep (($superclass eq $_), @superclassList));
    '';
};

sub new
{
    my $staticType = shift;
    my $SYSLOG = shift;
    my ($self);
    my ($className);
    my ($line);

    # check if we are repeating ourselves
    if ($gRepeatCount)
    {
	$line = $gLineToRepeat;
    }
    else
    {
	# read the next syslog line
	no strict 'refs';
	($line = <$SYSLOG>) || return undef;  # at EOF
	use strict 'refs';
	if (chop($line) ne "\n")
	{
	    warn "Discarding final line which was not newline-terminated.\n";
	    print STDERR "  (consider using 'tail -f syslog')\n";
	    return undef;
	}
    }
    
    # parse a line like: 'Jun 13 02:32:27 satellife mydaemon[25994]: foo'
    my ($month,$day,$time,$machine,$rest) =
	split ' ', $line, 5;

    # check for 'last line repeated n times' message
    if ($rest =~ /^last message repeated (\d+) time/)
    {
	! $gRepeatCount ||
	    die "repetition of 'last message repeated' line!?";
	$gRepeatCount = $1;
	$gLineToRepeat = $gLastLineByHost{$machine};
	($gFinalMonth, $gFinalDay, $gFinalTime) = ($month, $day, $time);
	$gRepeatCount ||
	    die "repetition of length 0!?";
	return SyslogScan::SyslogEntry -> new;
    }

    if ($gRepeatCount)
    {
	if ($gRepeatCount == 1)  # on last repetition
	{
	    ($month, $day, $time) = ($gFinalMonth, $gFinalDay, $gFinalTime);
	}
	else
	{
	    ($month, $day, $time) = ();  # cannot precisely know time
	}
	$gRepeatCount--;
    }

    $gLastLineByHost{$machine} = $line;
	
    my ($executable,$tag,$content) =
	$rest =~ /^([^\:\[\]]+)(\[\d+\])?\: (.*)/;
    $tag =~ s/\[(.+)\]/$1/ if defined $tag;

    if (! defined $executable)
    {
	$rest and
	    print STDERR "executable not defined in line: $line\n"
		unless $::gbQuiet;
    }

    # fill in my 'self' array
    $self = {
	"content" => $content,
	"month" => $month,
	"day" => $day,
	"time" => $time,
	"machine" => $machine,
	"executable" => $executable,
	"tag" => $tag,
	"raw" => $line
	};

    if (defined $time)
    {
	my $date = "$month $day $time";
	$self->{"unix_time"} = SyslogScan::ParseDate::parseDate($date);
    }

    # check for possible i/o error
    if ($line =~ /I\/O error/)
    {
	print STDERR "may be syslog I/O error in line:\n  $line\n";
	$$self{suspectIOError} = 1;
    }

    # Make first letter of program capital, and change . to _,
    # so the module to handle 'in.identd' is named "In_identdLine.pm"

    my $oldChar = substr($executable,0,1);
    substr($executable,0,1) =~ tr/a-z/A-Z/;
    my $handlerClass = "SyslogScan::" . $executable . "Line";
    $handlerClass =~ s/[\. ]/_/g;
    substr($executable,0,1) = $oldChar;

    # If the module to handle this program has been "use"'d,
    # then subclass our object and call its parseContent() method.
    if (&$pIsSubclass("SyslogScan::SyslogEntry",$handlerClass))
    {
	bless($self,$handlerClass);
    }
    elsif (defined ($gTable{$executable}))
    {
	bless($self,$gTable{$executable});
    }
    else
    {
	# this line is not supported by a handler class
	bless($self,"SyslogScan::UnsupportedEntry");
    }

    # TODO: get rid of 'type' in favor of checking ref
    eval
    {
	$self -> parseContent;
    };

    if ($@ ne "")
    {
	# provide "escape hatches" so a module can halt the
	# entire program execution if it really needs to
	if (($@ =~ /SYSLOGMODULEFATAL/) ||
	    defined $$self{"ERRORS ARE FATAL"})
	{
	    die "fatal module error: $@" ;
	}
	
	# catch non-fatal errors so flawed module does not break others
	my ($brokenHandler) = ref $self;
	bless ($self, "SyslogScan::BotchedEntry");
	$$self{"brokenHandler"} = $brokenHandler;
	$$self{"errorString"} = $@;
	print STDERR "SyslogEntry.pm caught $brokenHandler module error: \n" .
	    "  $@\n" .
	    "  returning BotchedEntry object\n";
    }

    $self;
}

sub parseContent
{
    my ($self) = @_;
    die "class ", ref($self), " did not override parseContent!\n";
}

# access methods

sub content    { return ( (my $self = shift)->{"content"});}
sub raw        { return ( (my $self = shift)->{"raw"});}
sub month      { return ( (my $self = shift)->{"month"});}
sub day        { return ( (my $self = shift)->{"day"});}
sub time       { return ( (my $self = shift)->{"time"});}
sub machine    { return ( (my $self = shift)->{"machine"});}
sub executable { return ( (my $self = shift)->{"executable"});}
sub tag        { return ( (my $self = shift)->{"tag"});}
sub unix_time  { return ( (my $self = shift)->{"unix_time"});}

1;

__END__

=head1 NAME

SyslogScan::SyslogEntry -- parses generic lines in syslog files.

=head1 SYNOPSIS

    use SyslogScan::SyslogEntry;

    open(FH,"/var/log/syslog");

    my $entry;

    # reads from filehandle $fh and returns an object
    # of a subclass of SyslogEntry.
    while ($entry = new SyslogScan::SyslogEntry (\*FH))
    {
	# process $entry
    }

=head1 DESCRIPTION

All Syslog object share these data structures: month, day, time,
machine, executable, tag (optional), content.

For example, if a syslog line looks like:

Jun 13 02:32:27 satellife in.identd[25994]: connect from mail.missouri.edu

then the line returned by 'new SyslogEntry' will return a
SyslogEntry-derived object with at least this set of parameters:

 month => Jun,
 day => 13,
 time => 02:32:27,
 machine => satellife,
 executable => in.identd,
 tag => 25994,
 content => connect from mail.missouri.edu,
 unix_time => 834633147,
 raw => Jun 13 02:32:27 satellife in.identd[25994]: connect from mail.missouri.edu

Since the executable is 'in.identd', SyslogEntry.pm will look for a
class called "SyslogScan::In_identdLine" derived from SyslogEntry, and
attempt to call that class's parseContent method.  If no such
In_identdLine class is in use, then the returned object is of the
default "SyslogScan::UnsupportedEntry" class.

If the In_identdLine class throws a die() exception, SyslogEntry.pm
will catch the die() and return a "SyslogScan::BotchedEntry" object
containing the exception in "$errorString" and the failed handler in
"brokenHandler".

"new SyslogEntry" returns the undefined value if at EOF.

=head1 BUGS

In retrospect, this model of passing control to subclasses based on
the name of the controlling program doesn't work exceptionally
elegantly in perl.  I would probably do it more conventionally if I
had it to do over again.

=head1 AUTHOR and COPYRIGHT

The author (Rolf Harold Nelson) can currently be e-mailed as
rolf@usa.healthnet.org.

Thanks to Allen S. Rout for his code contributions.

This code is Copyright (C) SatelLife, Inc. 1996.  All rights reserved.
This code is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

In no event shall SatelLife be liable to any party for direct,
indirect, special, incidental, or consequential damages arising out of
the use of this software and its documentation (including, but not
limited to, lost profits) even if the authors have been advised of the
possibility of such damage.

=head1 SEE ALSO

L<SyslogScan::SendmailLine>
