#!/usr/bin/perl

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell
#
#    pcapmerge - Perl program to merge packet capture files.
#
#    Author: Francis J. Lacoste <francis.lacoste@iNsu.COM>
#
#    Copyright (C) 2000 Francis J. Lacoste, iNsu Innovations
#
#    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; either version 2 of the License, or
#    (at your option) any later version.
#
#    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
#
use strict;

use Time::Local;
use Net::Pcap;
use Getopt::Long;


use vars qw( $DATE_MANIP $VERSION );



BEGIN {
    $VERSION = "1.0";

    $DATE_MANIP = 0;
    eval "use Date::Manip;";
    $DATE_MANIP = 1 unless $@;
}

my %opts = ();

sub version() {
print <<EOF;
pcapmerge 1.0
Copyright(c) Francis J. Lacoste, iNsu Innovations Inc.

Report bugs to <bugs\@iNsu.COM>

There is NO warranty.  You may redistribute this software
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING.
EOF
    exit;
}

sub usage($) {
    print <<EOF;
Usage: pcapmerge [ -w file ] [-s start] [-e end ] [-p period]
	         [ -r file ...] filters
Extract or merge part PCAP capture file.
  -s, --start	time	start time
  -e, --end	time	end time
  -p, --period	length	report period length
  -w, --output  file	write merge pcap file to this file rather than stdout
  -r, --input	file	capture file to merge / read
  -h            print this help message and exits.
  -v		display version information and exits.
EOF

    exit shift;
}

sub parse_date {
    my $str = shift;

    if ( $DATE_MANIP) {
	my $date = ParseDate( $str ) or return undef;
	return UnixDate( $date, '%s' );
    } else {
	my ( $yearpart, $year, $month, $day, $time, $hour, $min, $sec) =
	  $str =~ /((\d\d\d?\d?|\d\d)?-?(\d\d?)-(\d\d?) ?)?((\d\d?):(\d\d?):?(\d\d?)?)?/;
	return undef unless $yearpart ||  $time;

	if ( $yearpart ) {
	    if (defined $year) {
		$year = $year > 1900 ? $year - 1900 :
				       $year < 70 ? $year + 100 : $year;
	    } else {
		$year = (localtime)[5];
	    }
	    $month = $month == 12 ? 0 : $month - 1;
	} else {
	    ($year,$month,$day ) = (localtime)[5,4,3];
	}
	unless ($time) {
	    # Midnight
	    ($hour,$min,$sec) = (0,0,0);
	}
	$sec ||= 0;
	return timelocal $sec, $min, $hour, $day, $month, $year;
    }
}

sub parse_period {
    my $str = shift;
    if ( $DATE_MANIP ) {
	my $period = ParseDateDelta( $str ) or return undef;
	return Delta_Format( $period, 0, '%st' );
    } else {
	my ( $weeks, $days, $hours, $mins, $secs ) =
	  $str =~ /(?:(\d+) ?w[eks ]*)?(?:(\d+) ?d[ays ]*)?(?:(\d+) ?h[hours ]*)?(?:(\d+) ?m[inutes ]*)?(?:(\d+) ?s[econds ]*)?/i;

	my $time = 0;

	$time += $weeks * 7  * 24 * 60 * 60 if $weeks;
	$time += $days  * 24 * 60 * 60	    if $days;
	$time += $hours * 60 * 60	    if $hours;
	$time += $mins  * 60		    if $mins;
	$time += $secs			    if $secs;

	return $time || undef;
    }
}

my @packets = ();

GetOptions( \%opts, "start=s", "end=s", "period=s", "output|w=s", "input|r=s@",
	   "help|?", "version" )
  or usage(1);
usage(0) if $opts{help};
version  if $opts{version};
die "pcapmerge: output must not be a terminal\n"
  unless ( exists $opts{output} || ! -t STDOUT );

my $start  = undef;
my $end	   = time;
my $period = undef;
if ( $opts{start} ) {
    $start = parse_date( $opts{start} )
      or die "pcapmerge: invalid date format: $opts{start}\n";
}
if ( $opts{end} ) {
    $end = parse_date( $opts{end} )
      or die "pcapmerge: invalid date format: $opts{end}\n";
} elsif ($opts{period} ) {
    $period = parse_period( $opts{period} )
      or die "pcapmerge: invalid period: $opts{period}\n";
    $end = $start + $period if defined $start;
}

sub save_pkts {
    my $sec = $_[1]->{tv_sec};
    unless (defined $start) {
	$start = $_[1]->{tv_sec};
        $end   = $start + $period if defined $period;
    }

    if ( $sec >= $start && $sec <= $end ) {
	push @packets, [ $_[1], $_[2] ];
    }
}

# Read packets in

push @{$opts{input}}, "-" unless exists $opts{input};
my $pcap_t = undef;
my $filter_str = join " ", @ARGV;
foreach my $file ( @{$opts{input}} ) {
    my $err;

    # Keep reference to the last file opened
    Net::Pcap::close( $pcap_t ) if defined $pcap_t;

    $pcap_t = Net::Pcap::open_offline( $file, \$err )
      or die "pcapmerge: error opening file $file: $err\n";

    if ( $filter_str ) {
	my $filter = undef;
	my $r = Net::Pcap::compile( $pcap_t, \$filter, $filter_str, 1, 0 );
	if ( $r == -1 ) {
	    die "pcapmerge: invalid filter : ",
	      Net::Pcap::geterr( $pcap_t ),"\n";
	}
	Net::Pcap::setfilter( $pcap_t, $filter );
    }
    Net::Pcap::loop( $pcap_t, -1, \&save_pkts, undef );
}

#  Sort the packets according
@packets = sort { $a->[0]{tv_sec}  <=> $b->[0]{tv_sec} ||
		  $a->[0]{tv_usec} <=> $b->[0]{tv_usec}
		} @packets;
my $dumper_t = Net::Pcap::dump_open( $pcap_t, $opts{output} || "-" )
  or die "pcapmerge: error opening output file: ",
  Net::Pcap::geterr( $pcap_t ), "\n";
foreach my $p ( @packets ) {
    Net::Pcap::dump( $dumper_t, $p->[0], $p->[1] );
}
Net::Pcap::dump_close( $dumper_t );

exit;

__END__

=pod

=head1 NAME

pcapmerge - Program to merge and extract part of binary capture file.

=head1 SYNOPSIS

pcapmerge [ B<-r file> ... ] [ B<-w output> ] [ B<-s start> ] [ B<-e end> | B<-p period> ] bpf filter


=head1 DESCRIPTION

pcapmerge can be used to extract part of a binary packet capture file
or merge several capture files. It is similar in scope to the
tcpslice(1) program.

=head2 OPTIONS

=over

=item -r, --input

Specifies the input file to read. You can use this option several times to merge multiple files. Defaults is to read from STDIN.

=item -w, --output

Sets the file to which to write the merged/extracted data. Defaults to
stdout, which should be redirected to a file or piped to another program.

=item -s, --start

Sets the start time. No packets with a timestamp lower than this will
be output. Defaults is to output all packets.

If the Date::Manip(3) module is installed, you can use any format that
this module can parse. If that module is'nt installed you must use the
following format YYYY-MM-DD HH:MM:SS or any meaningful subset of that
format.

=item -e, --end

Sets the end time. No packets with a timestamp greater than this will be
output. Defaults is to output all packets.

If the Date::Manip(3) module is installed, you can use any format that
this module can parse. If that module is'nt installed you must use the
following format YYYY-MM-DD HH:MM:SS or any meaningful subset of that
format.

=item -p, --period

Specifies the maximum timestamp relative to the starting time.

If you have the Date::Manip module installed, you can use any format
that this module can parse. If that module isn't available, you can
use a subset of the following format X weeks X days X hours X mins X
secs.

=back

=head1 ARGUMENTS

Remaining arguments are interpreted as a tcpdump filter to restrict
output to the packets matching the filter. See tcpdump(8) for syntax.

=head1 AUTHOR

Copyright (c) 2000 Francis J. Lacoste and iNsu Innovations Inc.
All rights reserved.

There is NO warranty. You may redistribute this software under the
terms of the GNU General Public License. For more information about
these matters, see the file named COPYING.

=head1 SEE ALSO

Net::Pcap(3), pcap(3), tcpdump(8)

=cut


