#
# $Id: Dump.pm,v 1.3.2.9 2006/06/04 13:44:52 gomor Exp $
#
package Net::Packet::Dump;
use strict;
use warnings;
use Carp;

require Class::Gomor::Array;
our @ISA = qw(Class::Gomor::Array);

use Net::Packet::Env qw($Env);
require Net::Packet::Frame;
use Net::Packet::Utils qw(getRandom32bitsInt);
use Net::Packet::Consts qw(:dump);

use Net::Pcap;
use Time::HiRes qw(gettimeofday);
use Storable qw(lock_store lock_retrieve);

our @AS = qw(
   dev
   file
   filter
   overwrite
   timeoutOnNext
   timeout
   promisc
   link
   nextFrame
   isRunning
   unlinkOnClean
   noStore
   noLayerWipe
   mode
   _pid
   _pcapd
   _dumper
   _stats
   _firstTime
   _sName
   _sDataAwaiting
);
our @AA = qw(
   frames
);
our @AO = qw(
   framesSorted
);
__PACKAGE__->cgBuildIndices;
__PACKAGE__->cgBuildAccessorsScalar(\@AS);
__PACKAGE__->cgBuildAccessorsArray(\@AA);

no strict 'vars';

BEGIN {
   my $osname = {
      cygwin  => \&_killTcpdumpWin32,
      MSWin32 => \&_killTcpdumpWin32,
   };

   *_killTcpdump = $osname->{$^O} || \&_killTcpdumpOther;
}

sub new {
   my $self = shift->SUPER::new(
      dev        => $Env->dev,
      file       => "netpacket-tmp-$$.@{[getRandom32bitsInt()]}.pcap",
      filter     => '',
      overwrite  => 0,
      timeout    => 0,
      promisc    => 0,
      timeoutOnNext  => 3,
      isRunning      => 0,
      unlinkOnClean  => 1,
      noStore        => 0,
      noLayerWipe    => 0,
      framesSorted   => {},
      frames         => [],
      mode           => NP_DUMP_MODE_ONLINE,
      _sDataAwaiting => 0,
      _sName         => "netpacket-tmp-$$.@{[getRandom32bitsInt()]}.storable",
      @_,
   );

   unless ($self->[$__file]) {
      confess("You MUST set `file' attribute\n");
   }

   $Env->dump($self) unless $Env->noDumpAutoSet;

   $self;
}

sub isModeOnline  { shift->[$__mode] eq NP_DUMP_MODE_ONLINE  }
sub isModeOffline { shift->[$__mode] eq NP_DUMP_MODE_OFFLINE }
sub isModeWriter  { shift->[$__mode] eq NP_DUMP_MODE_WRITER  }

sub start {
   my $self = shift;

   $self->cgDebugPrint(1, 'will run in mode: '.$self->mode);

   $self->[$__isRunning] = 1;

   if ($self->isModeOnline) {
      if (-f $self->[$__file] && ! $self->[$__overwrite]) {
         croak("We will not overwrite a file by default. Use `overwrite' ".
               "attribute to do it\n");
      }
      $self->_sStore(0);
      $self->_waitFileSize($self->[$___sName]);
      $self->_startTcpdump;
      $self->_openFileOffline;
   }
   elsif ($self->isModeOffline) {
      if (! -f $self->[$__file]) {
         croak("File does not exists: ".$self->[$__file]."\n");
      }
      $self->_openFileOffline;
      $self->_setFilter;
   }
   elsif ($self->isModeWriter) {
      if (-f $self->[$__file] && ! $self->[$__overwrite]) {
         croak("We will not overwrite a file by default. Use `overwrite' ".
               "attribute to do it\n");
      }
      $self->_openFileWriter;
   }

   1;
}

sub stop {
   my $self = shift;

   return unless $self->[$__isRunning];
   return if     $self->isSon;

   if ($self->isModeOnline) {
      $self->_killTcpdump;
      $self->[$___pid] = undef;
      if ($self->[$___sName] && -f $self->[$___sName]) {
         unlink($self->[$___sName]);
      }
   }
   elsif ($self->isModeWriter) {
      Net::Pcap::dump_close($self->[$___dumper]);
   }
   elsif ($self->isModeOffline) {
      # Nothing to do here
   }

   Net::Pcap::close($self->[$___pcapd]);
   $self->[$__isRunning] = 0;

   1;
}

sub isFather {   shift->[$___pid] }
sub isSon    { ! shift->[$___pid] }

sub _sStore {
   lock_store(\$_[1], $_[0]->[$___sName])
      or carp("@{[(caller(0))[3]]}: lock_store: @{[$_[0]->[$___sName]]}: $!\n");
}
sub _sRetrieve { ${lock_retrieve(shift->[$___sName])} }

sub _sonPrintStats {
   my $self = shift;

   my $stats = $self->getStats;
   Net::Pcap::breakloop($self->[$___pcapd]);
   Net::Pcap::close($self->[$___pcapd]);

   $self->cgDebugPrint(1, 'Frames received  : '.$stats->{ps_recv});
   $self->cgDebugPrint(1, 'Frames dropped   : '.$stats->{ps_drop});
   $self->cgDebugPrint(1, 'Frames if dropped: '.$stats->{ps_ifdrop});
   exit(0);
}

sub _waitFile {
   my $self = shift;
   my ($file) = @_;
   my $startTime = gettimeofday();
   my $thisTime  = $startTime;
   while (! -f $file) {
      if ($thisTime - $startTime > 10) {
         croak("@{[(caller(0))[3]]}: too long for file creation: $file\n")
      }
      $thisTime = gettimeofday();
   }
}

sub _waitFileSize {
   my $self = shift;
   my ($file) = @_;

   $self->_waitFile($file);

   my $startTime = gettimeofday();
   my $thisTime  = $startTime;
   while (! ((stat($file))[7] > 0)) {
      if ($thisTime - $startTime > 10) {
         $self->clean;
         croak("@{[(caller(0))[3]]}: too long for file creation2: $file\n")
      }
      $thisTime = gettimeofday();
   }
}

sub _startTcpdump {
   my $self = shift;

   my $err;
   my $pd = Net::Pcap::open_live(
      $self->[$__dev],
      1514,
      $self->[$__promisc],
      1000,
      \$err,
   );
   unless ($pd) {
      croak("@{[(caller(0))[3]]}: open_live: $err\n");
   }

   my $net;
   my $mask;
   Net::Pcap::lookupnet($self->[$__dev], \$net, \$mask, \$err);
   if ($err) {
      croak("@{[(caller(0))[3]]}: lookupnet: $err\n");
   }

   my $fcode;
   if (Net::Pcap::compile($pd, \$fcode, $self->[$__filter], 0, $mask) < 0) {
      croak("@{[(caller(0))[3]]}: compile: ". Net::Pcap::geterr($pd). "\n");
   }

   if (Net::Pcap::setfilter($pd, $fcode) < 0) {
      croak("@{[(caller(0))[3]]}: setfilter: ". Net::Pcap::geterr($pd). "\n");
   }

   my $p = Net::Pcap::dump_open($pd, $self->[$__file]);
   unless ($p) {
      croak("@{[(caller(0))[3]]}: dump_open: ". Net::Pcap::geterr($pd). "\n");
   }
   Net::Pcap::dump_flush($p);

   $SIG{CHLD} = 'IGNORE';

   my $pid = fork();
   croak("@{[(caller(0))[3]]}: fork: $!\n") unless defined $pid;
   if ($pid) {
      $self->[$___pid] = $pid;
      return 1;
   }
   else {
      $self->[$___pcapd] = $pd;
      $SIG{INT}  = sub { $self->_sonPrintStats };
      $SIG{TERM} = sub { $self->_sonPrintStats };
      $self->cgDebugPrint(1, "dev:    [@{[$self->[$__dev]]}]\n".
                             "file:   [@{[$self->[$__file]]}]\n".
                             "filter: [@{[$self->[$__filter]]}]");
      Net::Pcap::loop($pd, -1, \&_tcpdumpCallback, [ $p, $self ]);
      Net::Pcap::close($pd);
      exit(0);
   }
}

sub _tcpdumpCallback {
   my ($data, $hdr, $pkt) = @_;
   my $p    = $data->[0];
   my $self = $data->[1];

   Net::Pcap::dump($p, $hdr, $pkt);
   Net::Pcap::dump_flush($p);

   my $n = $self->_sRetrieve;
   $self->_sStore(++$n);
}

sub _killTcpdumpWin32 {
   my $self = shift;
   return unless $self->[$___pid];
   kill('KILL', $self->[$___pid]);
}

sub _killTcpdumpOther {
   my $self = shift;
   return unless $self->[$___pid];
   kill('TERM', $self->[$___pid]);
}

sub clean {
   my $self = shift;

   if ($self->isModeOnline) {
      if ($self->[$__unlinkOnClean]
      &&  $self->[$__file] && -f $self->[$__file]) {
         unlink($self->[$__file]);
         $self->cgDebugPrint(1, "@{[$self->[$__file]]} removed");
      }
   }

   1;
}

sub getStats {
   my $self = shift;

   unless ($self->[$___pcapd]) {
      carp("@{[(caller(0))[3]]}: unable to get stats, no pcap descriptor ".
           "opened\n");
      return undef;
   }
   
   my %stats;
   Net::Pcap::stats($self->[$___pcapd], \%stats);
   $self->[$___stats] = \%stats;
   \%stats;
}

sub flush {
   my $self = shift;
   $self->[$__frames]       = [];
   $self->[$__framesSorted] = {};
}

sub _setFilter {
   my $self = shift;
   my $str = $self->[$__filter];

   return unless $str;

   my ($net, $mask, $err);
   Net::Pcap::lookupnet($self->[$__dev], \$net, \$mask, \$err);
   if ($err) {
      croak("@{[(caller(0))[3]]}: Net::Pcap::lookupnet: @{[$self->[$__dev]]}: ".
            "$err\n");
   }

   my $filter;
   Net::Pcap::compile($self->[$___pcapd], \$filter, $str, 0,
                      $mask);
   unless ($filter) {
      croak("@{[(caller(0))[3]]}: Net::Pcap::compile: error\n");
   }

   Net::Pcap::setfilter($self->[$___pcapd], $filter);
}

sub _openFileOffline {
   my $self = shift;

   my $err;
   $self->[$___pcapd] = Net::Pcap::open_offline($self->[$__file], \$err);
   unless ($self->[$___pcapd]) {
      croak("@{[(caller(0))[3]]}: Net::Pcap::open_offline: ".
            "@{[$self->[$__file]]}: $err\n");
   }

   $self->[$__link] = Net::Pcap::datalink($self->[$___pcapd]);
}

sub _getPcapHeader {
   my $self = shift;
   # 24 bytes header of a DLT_RAW pcap file
   "\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00".
   "\x00\x00\x00\x00\xdc\x05\x00\x00\x0c\x00\x00\x00";
}

sub _openFileWriter {
   my $self = shift;
   my $file = $self->[$__file];

   my $hdr = $self->_getPcapHeader;
   open(my $fh, '>', $file)
      or croak("@{[(caller(0))[3]]}: open: $file: $!\n");
   syswrite($fh, $hdr, length($hdr));
   close($fh);

   my $err;
   my $pcapd = Net::Pcap::open_offline($file, \$err);
   unless ($pcapd) {
      croak("@{[(caller(0))[3]]}: Net::Pcap::open_offline: ".
            "$file: $err\n");
   }
   $self->[$___pcapd] = $pcapd;

   $self->[$___dumper] = Net::Pcap::dump_open($pcapd, $file);
   unless ($self->[$___dumper]) {
      croak("@{[(caller(0))[3]]}: Net::Pcap::dump_open: ".
            Net::Pcap::geterr($pcapd)."\n");
   }

   1;
}

sub _addToFramesSorted {
   my $self = shift;
   my ($frame) = @_;
   $self->framesSorted($frame);
   push @{$self->[$__frames]}, $frame;
}

sub _pcapNext {
   my $self = shift;
   my %hdr;
   if (my $raw = Net::Pcap::next($self->[$___pcapd], \%hdr)) {
      $hdr{tv_usec} = sprintf("%06d", $hdr{tv_usec});
      my $frame = Net::Packet::Frame->new(
         raw       => $raw,
         timestamp => "$hdr{tv_sec}.$hdr{tv_usec}",
      ) or return undef;

      $self->_addToFramesSorted($frame) unless $self->[$__noStore];
      return $frame;
   }
   undef;
}

sub _getNextAwaitingFrameOffline {
   my $self = shift;
   $self->_pcapNext;
}

sub _getNextAwaitingFrameOnline {
   my $self = shift;
   my $last = $self->[$___sDataAwaiting];
   my $new  = $self->_sRetrieve;

   # Return if nothing new is awaiting
   return undef if ($new <= $last);

   $self->[$___sDataAwaiting]++;
   $self->_pcapNext;
}

sub _getNextAwaitingFrame {
   my $self = shift;
   $self->isModeOnline ? $self->_getNextAwaitingFrameOnline
                       : $self->_getNextAwaitingFrameOffline;
}

# XXX: need more work
#sub _getNextAwaitingFrames {
   #my $self = shift;
   #my $last = $self->[$___sDataAwaiting];
   #my $new  = $self->_sRetrieve;
   #my $diff = $new - $last;
   #return [] if $diff <= 0; # Nothing awaiting
   #$self->[$___sDataAwaiting] += $diff;
   #my $frames = [];
   #while ($diff--) {
      #push @$frames, $self->_pcapNext;
   #}
   #$frames;
#}

sub _nextTimeoutHandle {
   my $self = shift;

   # Handle timeout
   my $thisTime = gettimeofday()      if     $self->[$__timeoutOnNext];
   $self->[$___firstTime] = $thisTime unless $self->[$___firstTime];

   if ($self->[$__timeoutOnNext] && $self->[$___firstTime]) {
      if (($thisTime - $self->[$___firstTime]) > $self->[$__timeoutOnNext]) {
         $self->[$__timeout]    = 1;
         $self->[$___firstTime] = 0;
         $self->cgDebugPrint(1, "Timeout occured");
         return undef;
      }
   }
   1;
}

sub _nextTimeoutReset { shift->[$___firstTime] = 0 }

sub next {
   my $self = shift;

   unless ($self->[$__isRunning]) {
      croak("You MUST call start() method before using next() method\n");
   }

   $self->_nextTimeoutHandle or return undef;

   my $frame = $self->_getNextAwaitingFrame;
   $self->_nextTimeoutReset if $frame;

   $frame ? do { $self->[$__nextFrame] = $frame } : undef;
}

sub nextAll { my $self = shift; while ($self->next) {} }

sub write {
   my $self = shift;
   my ($frame) = @_;

   unless ($self->isModeWriter) {
      croak("Dump is not in writer mode\n");
   }

   # Rebuild the frame without possible layer 2
   my $new;
   $new .= $frame->l3->raw if $frame->l3;
   $new .= $frame->l4->raw if $frame->l4;
   $new .= $frame->l7->raw if $frame->l7;

   # Create pcap header
   my ($sec, $usec) = split('\.', $frame->timestamp);
   my $hdr = {
      len     => length($new),
      caplen  => length($new),
      tv_sec  => $sec,
      tv_usec => $usec,
   };

   Net::Pcap::pcap_dump($self->[$___dumper], $hdr, $new);
   Net::Pcap::dump_flush($self->[$___dumper]);
}

# XXX: broken for now
#sub nextAll {
   #my $self = shift;
   #$self->_nextTimeoutHandle or return [];

   #my $frames = $self->_getNextAwaitingFrames;
   #$self->_nextTimeoutReset if @$frames;
   #$frames;
#}

sub timeoutReset { shift->[$__timeout] = 0 }

sub framesFor {
   my $self = shift;
   my ($f) = @_;

   my $l2Key = ($f->l2 && $f->l2->getKeyReverse($f)) || 'all';
   my $l3Key = ($f->l3 && $f->l3->getKeyReverse($f)) || 'all';
   my $l4Key = ($f->l4 && $f->l4->getKeyReverse($f)) || 'all';
   my $aref = $self->[$__framesSorted]->{$l2Key}{$l3Key}{$l4Key};

   $aref ? @$aref : ();
}

#
# Other accessors
#

sub framesSorted {
   my $self = shift;
   my ($f) = @_;

   if ($f) {
      # Wipe headers, since if not, framesFor() will not be able to find them.
      # Because if you create a Frame from L3, no headers are set for L2, but 
      # the Dump will have them and store them into the l2Key.
      if ($Env->desc && ! $self->[$__noLayerWipe]) {
         $f->l2(undef) if ref($Env->desc) =~ /L3|L4/;
         $f->l3(undef) if ref($Env->desc) =~ /L4/;
      }

      my $l2Key = ($f->l2 && $f->l2->getKey($f)) || 'all';
      my $l3Key = ($f->l3 && $f->l3->getKey($f)) || 'all';
      my $l4Key = ($f->l4 && $f->l4->getKey($f)) || 'all';
      push @{$self->[$__framesSorted]->{$l2Key}{$l3Key}{$l4Key}}, $f;

      # We store a second time for ICMP messages
      if ($f->isIcmp) {
         my $l3Key = ($f->l3 && $f->l3->is.':'.$f->l3->dst) || 'all';
         push @{$self->[$__framesSorted]->{$l2Key}{$l3Key}{$l4Key}}, $f;
      }
   }

   $self->[$__framesSorted];
}

1;

__END__

=head1 NAME

Net::Packet::Dump - a tcpdump-like object providing frame capturing

=head1 SYNOPSIS

   use Net::Packet::Dump;

   #
   # Example live capture (sniffer like)
   #

   # Instanciate object, will start capturing from network
   my $dump = Net::Packet::Dump->new(
      filter  => 'tcp',
      noStore => 1,
   );

   while (1) {
      if (my $frame = $dump->next) {
         print $frame->l2->print, "\n" if $frame->l2;
         print $frame->l3->print, "\n" if $frame->l3;
         print $frame->l4->print, "\n" if $frame->l4;
         print $frame->l7->print, "\n" if $frame->l7;
      }
   }

   #
   # Example offline analysis
   #

   my $dump2 = Net::Packet::Dump->new(
      unlinkOnClean => 0,
      file          => 'existant-file.pcap',
   );

   # Analyze the .pcap file, build an array of Net::Packet::Frame's
   $dump->analyze;

   # Browses captured frames
   for ($dump->frames) {
      # Do what you want
      print $_->l2->print, "\n" if $_->l2;
      print $_->l3->print, "\n" if $_->l3;
      print $_->l4->print, "\n" if $_->l4;
      print $_->l7->print, "\n" if $_->l7;
   }

=head1 DESCRIPTION

This module is the capturing part of Net::Packet framework. It is basically a tcpdump process. When a capture starts, the tcpdump process is forked, and saves all traffic to a .pcap file. The parent process can call B<next>, B<nextAll> or B<analyze> to convert captured frames from .pcap file to B<Net::Packet::Frame>s.

Then, you can call B<recv> method on your sent frames to see if a corresponding reply is waiting in the B<frames> array attribute of B<Net::Packet::Dump>.

By default, if you use this module to analyze frames you've sent (very likely ;)), and you've sent those frames at layer 4 (using B<Net::Packet::DescL4>) (for example), lower layers will be wiped on storing in B<frames> array. This behaviour can be disabled using B<noLayerWipe> attribute.

=head1 ATTRIBUTES

=over 4

=item B<env>

Stores a B<Net::Packet::Env> object. It is used in B<start> method, for example. The default is to use the global B<$Env> object created when using B<Net::Packet>.

=item B<file>

Where to save captured frames. By default, a random name file is chosen, named like `netpacket-tmp-$$.@{[getRandom32bitsInt()]}.pcap'.

=item B<filter>

A pcap filter to restrain what to capture. It also works in offline mode, to analyze only what you want, and not all traffic. Default to capture all traffic. WARNING: every time a packet passes this filter, and the B<next> method is called, the internal counter used by b<timeoutOnNext> is reset. So the B<timeout> attribute can only be used if you now exactly that the filter will only catch what you want and not perturbating traffic.

=item B<overwrite>

If the B<file> exists, setting this to 1 will overwrite it. Default to not overwrite it.

=item B<timeout>

Is auto set to 1 when a timeout has occured. It is not set to 0 automatically, you need to do it yourself.

=item B<timeoutOnNext>

Each time B<next> method is called, an internal counter is incremented if no frame has been captured. When a frame is captured (that is, a frame passed the pcap filter), the B<timeout> attribute is reset to 0. When the counter reaches the value of B<timeoutOnNext>, the B<timeout> attribute is set to 1, meaning no frames have been captured during the specified amount of time. Default to 3 seconds.

=item B<nextFrame>

This one stores the latest received frame after a call to B<next> method. If a B<next> call is done, and no frame is received, this attribute is set to undef.

=item B<isRunning>

When the capturing process is running, this is set to 1. So, when B<start> method has been called, it is set to 1, and when B<stop> method is called, set to 0.

=item B<unlinkOnDestroy>

When the B<Net::Packet::Dump> object goes out of scope, the B<DESTROY> method is called, and if this attribute is set to 1, the B<file> is removed. BEWARE: default to 1.

=item B<noStore>

If you set this attribute to 1, frames will not be stored in B<frames> array. It is used in sniffer-like programs, in order to avoid memory exhaustion by keeping all captured B<Net::Packet::Frame> into memory. Default is to store frames.

=item B<noLayerWipe>

As explained in DESCRIPTION, if you send packets at layer 4, layer 2 and 3 are not keeped when stored in B<frames>. The same is true when sending at layer 3 (layer 2 is not kept). Default to wipe those layers. WARNING: if you set it to 1, and you need the B<recv> method from B<Net::Packet::Frame>, it will fail. In fact, this is a speed improvements, that is in order to find matching frame for your request, they are stored in a hash, using layer as keys (B<getKey> and B<getKeyReverse> are used to get keys from each layer. So, if you do not wipe layers, a key will be used to store the frame, but another will be used to search for it, and no match will be found. This is a current limitation I'm working on to remove.

=item B<noEnvSet>

By default, when a B<Net::Packet::Dump> object is created, the default B<$Env> object has its B<dump> attribute pointing to it. If you do not want this behaviour, you can disable it by setting it to 1. Default to 0.

=item B<frames>

Stores all analyzed frames found in a pcap file in this array.

=item B<framesSorted>

Stores all analyzed frames found in a pcap file in this hash, using keys to store and search related packet request/replies.

=back

=head1 METHODS

=over 4

=item B<new>

Object contructor. Default values for attributes:

env:             $Env

file:            "netpacket-tmp-$$.@{[getRandom32bitsInt()]}.pcap"

filter:          ""

overwrite:       0

timeout:         0

timeoutOnNext:   3

isRunning:       0

unlinkOnDestroy: 1

noStore:         0

noLayerWipe:     0

noEnvSet:        0

=item B<start>

Forks the tcpdump-like process that do frame capturing saved to a file. It does not forks a new process if the specified B<file> attribute exists, and B<overwrite> attributes is set to 0. It also sets B<isRunning> to 1 if a process is forked.

=item B<getStats>

Tries to get packet statistics on an open descriptor. It returns a reference to a hash that has to following fields: B<ps_recv>, B<ps_drop>, B<ps_ifdrop>.

=item B<stop>

Kills the tcpdump-like process, and sets B<isRunning> to 0.

=item B<flush>

Will removed all analyzed frames from B<frames> array and B<framesSorted> hash. Use it with caution, because B<recv> from B<Net::Packet::Frame> relies on those.

=item B<next>

Returns the next captured frames; undef if none found in .pcap file. In all cases, B<nextFrame> attribute is set (either to the captured frame or undef). Each time this method is run, a comparison is done to see if no frame has been captured during B<timeoutOnNext> amount of seconds. If so, B<timeout> attribute is set to 1 to reflect the pending timeout. When a frame is received, it is stored in B<frames> array, and in B<framesSorted> hash, used to quickly B<recv> it (see B<Net::Packet::Frame>), and internal counter for time elapsed since last received packet is reset.

=item B<nextAll>

=item B<analyze>

Calls B<next> method until it returns undef (meaning no new frame waiting to be analyzed from pcap file).

=item B<framesFor> (scalar)

You pass a B<Net::Packet::Frame> has parameter, and it returns an array of all frames relating to the connection. For example, when you send a TCP SYN packet, this method will return TCP packets relating to the used source/destination IP, source/destination port, and also related ICMP packets.

=back

=head1 AUTHOR

Patrice E<lt>GomoRE<gt> Auffret

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2004-2006, Patrice E<lt>GomoRE<gt> Auffret

You may distribute this module under the terms of the Artistic license.
See LICENSE.Artistic file in the source distribution archive.

=head1 RELATED MODULES

L<NetPacket>, L<Net::RawIP>, L<Net::RawSock>

=cut
