#!/usr/bin/perl
use strict;
#############################################################################
#
# ppplag.pl -- PPP log analyzer grid :)
# Vladi Belperchinov-Shabanski
# <cade@biscom.net> <cade@datamax.bg>
# http://www.biscom.net/~cade
# Any notes, comments, suggestions are welcome! Thank you!
#
# Contributors:
#
# Lewi Supranata .K (ichtus)
# email: ichtus@mbone.petra.ac.id, 
# homepage: http://mbone.petra.ac.id/mypages/ichtus
#
# Thanx!
#
# ChangeLog/History:
#
# 04.Nov.2000 -- initial release
# 09.Nov.2001 -- adsl pppd messages support
# 20.Nov.2001 -- filters only pppd log lines
# 09.Dec.2001 -- multi-files (+.gz), -s, -b, -m options(s)
# 04.Jan.2002 -- lock on pid, -i option(s)
# 06.May.2003 -- -a and -e options
# 09.May.2003 -- -u option
#                thanx to Robert Hoekstra
#                <robert -DOT- hoekstra -AT- parox -DOT- nl> for this patch
#                -l option added
# 11.May.2003 -- PPPLAG_LOGFILE env var added
#
# 26.Jul.2003 -- Lewi <ichtus@mbone.petra.ac.id>
#                - TelkomNet cost calculation added as default
#                - new time format
#                - cost round up added
#                - ISP month fee
#                TODO: 
#                - support for multiple ISP
#                - per month report?
#                - support for cost peak time
# 27.Feb.2005 -- -d, -r and -n
#                thanks to Andrea Spadaccini <lupin85 -AT- email -DOT- it> 
#                for the hints
#
#############################################################################
# config section -- edit as you want/need
#############################################################################

my $VERSION = '$Id: ppplag.pl,v 1.15 2005/02/27 19:56:33 cade Exp $';

my $opt_sumonly = 0;       # show only summary?
my $opt_summon = 0;        # show month summaries only?
my $opt_sumday = 0;        # show day summaries only?
my $opt_bio = 0;           # show bps/in/out? 	
my $opt_auth_user_in = 0;  # show auth user in (server-side)?
my $opt_auth_user_out = 0; # show auth user out (client-side)?
my $opt_if = "--";         # interface? ppp0, ppp1, ttyS0, ttyS1, etc
                           # this will match anytime `ppp0 <--> /dev/modem'
my $opt_ruser;

my $opt_showcost = 1;      # show cost summary?

my $opt_begin_after;
my $opt_end_after;
my $opt_remote_ip = 0;
my $opt_local_ip = 0;

# costs calculations (Lewi)
my $curr       = $ENV{ PPPLAG_CURR } || "Curr"; # Currency label
my $costs      = $ENV{ PPPLAG_COST } || 1;      # Costs per unit
my $timing     = $ENV{ PPPLAG_UNIT } || 1;      # time unit in minutes
my $month_fee  = $ENV{ PPPLAG_FEE  } || 0;      # ISP fee per month

#############################################################################
# config section end -- don't edit below...
# everything below this line is just code -- nothing much to see there :)
#############################################################################

if( $ARGV[0] =~ /-?-h(elp)?/i )
  {
  die "ppplag.pl by Vladi Belperchinov-Shabanski 'Cade' 2001\n$VERSION\n" .
      "usage: ppplag.pl [options] logfile...\n" .
      "options:\n" .
      "   -s       show summary only\n" .
      "   -u       show auth-in user (server-side pppd logs)\n" .
      "   -l       show auth-out user (client-side pppd logs)\n" .
      "            (this requires pppd `debug' messages!)\n" .
      "   -b       show bps/in/out (hide costs)\n" .
      "   -n       suppress costs column\n" .
      "   -m       show summaries by month\n" .
      "   -d       show summaries by day\n" .
      "   -i if    use specific interface (ppp0,ttyS1,eth0,etc...)\n" .
      "   -a str   begin stats after `str' found\n" .
      "   -e str   end   stats after `str' found\n" .
      "   -z       show local ip\n" .
      "   -x       show remote ip\n" .
      "   -r user  show only connections started by 'user'\n" .
      "            example: -a 'Apr 10 11:11'\n" .
      "            example: -e 'Mar 12 12:12' -r cade\n" .
      "";
  }

my @LOG;
 
while( $_ = shift @ARGV )
  {
  $opt_local_ip = 1 if /^-z/;
  $opt_remote_ip = 1 if /^-x/;
  $opt_sumonly = 1 if /^-s/;
  $opt_bio = 1 if /^-b/;
  $opt_showcost = 0 if /^-n/;
  $opt_summon = 1 if /^-m/;
  $opt_sumday = 1 if /^-d/;
  $opt_auth_user_in = 1 if /^-u/;
  $opt_auth_user_out = 1 if /^-l/;
  $opt_if = shift @ARGV if /^-i/;
  $opt_ruser = shift @ARGV if /^-r/;
  $opt_begin_after = shift @ARGV if /^-a/;
  $opt_end_after = shift @ARGV if /^-e/;
  next if /^-/;
  push @LOG, $_;
  }

push @LOG, ( $ENV{ PPPLAG_LOGFILE } || '/var/log/messages' ) unless @LOG;

if ( $opt_sumonly or $opt_summon )
  {
  $opt_auth_user_in = 0;
  $opt_auth_user_out = 0;
  }

my @logformat = ( 
                  12, # 0 label 
                   0, # 1 user auth-in
                   0, # 2 user auth-out
                   8, # 3 time
                  12, # 4 cost
                  14, # 5 bytes in
                  14, # 6 bytes out
                   0, # 7 bps/in
                   0, # 8 bps/out
                   0, # 9 remote ip
                   0, # 10 local ip
                ); 

$logformat[1]  = 10 if $opt_auth_user_in;  # user auth-in  (server-side logs)
$logformat[2]  = 10 if $opt_auth_user_out; # user auth-out (client-side logs)
$logformat[4]  =  0 if not $opt_showcost or $opt_bio; # hide costs
$logformat[7]  =  8 if $opt_bio; # show bps
$logformat[8]  =  8 if $opt_bio; # show bps 
$logformat[9]  = 15 if $opt_remote_ip; # remote ip
$logformat[10] = 15 if $opt_local_ip; # remote ip

for my $log ( @LOG ) { process_log( $log ); }

#############################################################################
# process log sub
#############################################################################

my $best_speed_in  = 0;
my $best_speed_out = 0;

sub process_log
{
  my $log = shift;
  if ( ! -r $log )
    {
    print "ppplag.pl: cannot read $log, cannot continue\n";
    exit;
    }
 
  my $sep = "-" x 79 . "\n"; 
 
  my $total_time = 0;
  my $total_in   = 0;
  my $total_out  = 0;
  my $total_cost  = 0; 
 
  my $best_time  = 0;
  my $best_in    = 0;
  my $best_out   = 0;
  my $best_cost  = 0; 
 
  my $last_mon  = '';
  my $mon_time  = 0;
  my $mon_in    = 0;
  my $mon_out   = 0;

  my $last_day  = '';
  my $day_time  = 0;
  my $day_in    = 0;
  my $day_out   = 0;
 
  my $last_ruser;

  my $first_string = "Start Time";
  $first_string = "Day" if $opt_sumday;
  
  a_printf( \@logformat, $first_string, "User IN", "User OUT",  "Time", "Cost($curr)", "Bytes IN", "Bytes OUT", "Bps/IN", "Bps/OUT", "Remote IP", "Local IP" );
 
  print $sep unless $opt_sumonly;

  my $inside = defined $opt_begin_after ? 0 : 1;
 
  $_ = $log;
  open( my $i, /\.gz$/ ? "gzip -dc $log|" : "$log" );
  MAINLOOP:
  while(<$i>)
    {
      $last_ruser = $1 if /pppd \S+ started by (\S+?),/;
      if ( /$opt_if/ and /pppd\[(\d+)\]: +Connect: +/ )
        {
        next if $opt_ruser and $opt_ruser ne $last_ruser;
        if( defined $opt_begin_after )
          {
          $inside = 1 if index( $_, $opt_begin_after ) > -1;
          next unless $inside;
          }
        if( defined $opt_end_after )
          {
          close $i if index( $_, $opt_end_after ) > -1;
          }
   
        my $ppid = $1;
   
        my $this_mon;
        my $this_day;
        my $this_start;
        my $this_time = 0;
        my $this_in   = 0;
        my $this_out  = 0;
        my $cost 	    = 0; 
        my $user_in  = 'n/a'; # server-side user (auth-in)
        my $user_out = 'n/a'; # client-side user (auth-out) FIXME: pap only?
        my $remote_ip = 'n/a';
        my $local_ip = 'n/a';
   
        ( $this_start, $this_mon, $this_day ) = ( $1, $3, $2 ) if /^(((...) ..) ..:..):../;
        $last_mon = $this_mon unless $last_mon;
        $last_day = $this_day unless $last_day;
  
        while(<$i>)
          {
          redo MAINLOOP if /$opt_if/ and /pppd\[(\d+)\]: +Connect: +/;
          next unless /pppd\[$ppid\]/;
          # last if /Exit\./ or /Doing disconnect/;


#### Attention: use only _one_ of the following lines!!!
## following is the  original line to match normal PPTPserver logs
#$user_in = $1 if /peer authentication succeeded for (.*)/;

## next line is to be used on a COLLAX system to match it´s PPTP logs
$user_in = $1 if /pptpd-logwtmp.so ip-up ppp\d(.*) /;

          $user_out = $1 if /user="([^\"]+)"/;
          $this_time = $1 if /Connect time ([\d\.]+) minutes\./;
          $local_ip  = $1 if /local  IP address ([\d\.]+)/i;
          $remote_ip = $1 if /remote IP address ([\d\.]+)/i;
        if ( /Sent (\d+) bytes, received (\d+) bytes\./ )
            {
            $this_out = $1;
            $this_in  = $2;
            last;
            };
          }
        next if $this_time == 0;
   
        if( $last_day && $last_day ne $this_day )
          {
          print_line( $last_day, $user_in, $user_out, $day_time, $total_cost, $day_in, $day_out, $remote_ip, $local_ip ) 
            if !$opt_sumonly and $opt_sumday;
   
          $last_day = $this_day;
          $day_time = 0;
          $day_in = 0;
          $day_out = 0;
          }
   
        if( $last_mon && $last_mon ne $this_mon )
          {
          print_line( $last_mon, $user_in, $user_out, $mon_time, $total_cost, $mon_in, $mon_out, $remote_ip, $local_ip ) 
            if !$opt_sumonly and $opt_summon;
   
          $last_mon = $this_mon;
          $mon_time = 0;
          $mon_in = 0;
          $mon_out = 0;
          }
  
        ## cost calculation from $this_time
        # 604800 minute, max 1 week. value 0.01 is to get round .0 value, inquirer - need to be fix!
        my $sec = int( ($this_time - int($this_time % 604800) + 0.01) * 60);
        # rounded second to 1 minute
        my $_this_time = $this_time; if ($sec == 60) { $sec = 0; $_this_time += 1.0; }
        # check for minute
        my $minute = int( ($_this_time % 60) % $timing );
        if ($sec == 0 && $minute == 0) {
          $cost = int($this_time) / $timing * $costs;
          }
        elsif ( ($minute != 0) || ($timing == 1 && $sec != 0) || ($minute == 0 && $sec != 0) ) {
          $cost = int( (int($this_time) / $timing) + 1) * $costs;
          }
        ## end cost calculation
  
        print_line( $this_start, $user_in, $user_out, $this_time, $cost, $this_in, $this_out, $remote_ip, $local_ip  ) 
          unless $opt_sumonly or $opt_summon or $opt_sumday;
   
        $best_time   = $this_time  if $this_time > $best_time;
        $best_in     = $this_in    if $this_in   > $best_in;
        $best_out    = $this_out   if $this_out  > $best_out;
        $best_cost   = $cost       if $cost  > $best_cost; 
   
        $day_time   += $this_time;
        $day_in     += $this_in;
        $day_out    += $this_out;
        
        $mon_time   += $this_time;
        $mon_in     += $this_in;
        $mon_out    += $this_out;
   
        $total_time += $this_time;
        $total_in   += $this_in;
        $total_out  += $this_out;
        $total_cost += $cost;
        }
    }

  # month fee, added to total cost, calculation is per month
  if ($month_fee != 0) { $total_cost = $total_cost + $month_fee }

  print_line( $last_day, "", "", $day_time, $total_cost, $day_in, $day_out) 
    if !$opt_sumonly and $opt_sumday;
  print_line( $last_mon, "", "", $mon_time, $total_cost, $mon_in, $mon_out) 
    if !$opt_sumonly and $opt_summon;
  close $i;
  print $sep;
 
  print_line( "Best:",  "", "", $best_time, $best_cost, $best_in, $best_out); 
  print_line( "Total:", "", "", $total_time, $total_cost, $total_in, $total_out); 
 
  print $sep;
}

sub print_line
{
  my $_label = shift;
  my $_user_in = shift;
  my $_user_out = shift;
  my $_time = shift;
  my $_cost = shift; 
  my $_in = shift;
  my $_out = shift;
  my $_remote_ip = shift;
  my $_local_ip = shift;
 
  my $_time_str = "";
  # value 0.01 is to get round .0 value, inquirer - need to be fix!
  my $sec = int( ($_time - int($_time % 604800) + 0.01) * 60); 
  # round second to 1 minute
  if ($sec == 60) { $sec = 0; $_time += 1.0;}
 
  # new time format, more readable format
  if ( int($_time / 60) != 0 ) 
     {
     $_time_str = sprintf "%sh %02sm", int($_time / 60), int($_time) % 60;
     } 
   elsif ( ($_time % 60) != 0 ) 
     { 
     $_time_str = sprintf "%02sm", int($_time) % 60;
     } 
   elsif ($sec != 0) 
     { 
     $_time_str = sprintf ".1m"; 
     } 

  my $_speed_in  = $_time > 0 ? int(( $_in  ) / ( $_time * 60 )) : 'n/a';
  my $_speed_out = $_time > 0 ? int(( $_out ) / ( $_time * 60 )) : 'n/a';

  $best_speed_in  = $_speed_in  if $_speed_in  > $best_speed_in ;
  $best_speed_out = $_speed_out if $_speed_out > $best_speed_out;

  a_printf( \@logformat, $_label, $_user_in, $_user_out, $_time_str, comma($_cost), comma($_in), comma($_out), comma($_speed_in), comma($_speed_out), $_remote_ip, $_local_ip );
}

#############################################################################
# utility functions
#############################################################################

sub a_printf
{
  my $ar = shift;
  my @a = @$ar;
  my $s;
  for( @_ )
    {
    my $n = shift @a;
    next unless $n > 0;
    my $__ = substr( $_, 0, $n );
    $s .= sprintf "%" . $n . "s ", $__;
    }
  print $s . "\n";
}

sub comma
{
  my $res = reverse shift;
  $res =~ s/(\d\d\d)/$1,/go;
  $res =~ s/,$//;
  $res = reverse $res;
}

sub curr
{
  my $res = reverse shift;
  $res =~ s/(\d\d\d)/$1,/go;
  $res =~ s/,$//;
  $res = reverse $res;
  $res = $curr . " " . $res;
}

#############################################################################
# eof
#############################################################################

