#!/usr/bin/env perl
# -*- Perl -*-
#
# Copyright 1996 Massachusetts Institute of Technology
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that both the above copyright notice and this
# permission notice appear in all copies, that both the above
# copyright notice and this permission notice appear in all
# supporting documentation, and that the name of M.I.T. not be used
# in advertising or publicity pertaining to distribution of the
# software without specific, written prior permission.  M.I.T. makes
# no representations about the suitability of this software for any
# purpose.  It is provided "as is" without express or implied
# warranty.
# 
# THIS SOFTWARE IS PROVIDED BY M.I.T. ``AS IS''.  M.I.T. DISCLAIMS
# ALL EXPRESS OR IMPLIED WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT
# SHALL M.I.T. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

# defaults:
$Plotwindow = 1;
$PlotTemplate = '$from."-".$to.".xplot"';
$Tcpdump = 'STDIN';
$Usage_first = 1;
$BreakOnSyns = 0;
$EndOnFins = 0;
$Quiet = 0;
$Cumulative = 0;
$TimeConvert = 0;
$ForceRelative = 0;
$FinThreshold = 1; # seconds
$GzipOutput = 0;

# other initializations
#$Packets;
#$Bytes;
#$StartTime = 0;
#$LastTime;
#$State;	# 'synSent', 'synRecvd', 'synackSent', 'synackRecvd', 'finSent', 'finRecvd', 'finackSent', 'finackRecvd'
#$ListFile;

sub usage
{
    return if (!$Usage_first);
    $Usage_first = 0;
    print <<"END_OF_USAGE";
Usage: $0 [-w] [-s] [-c] [-plot[filename]] [-list[filename]] [-?] [-help]
-w: plot window.
-s: break up conversations on syns.
-f: ignore socket activity after a fin (until socket is re-used)
-c: cumulative - adds all data coming from a server
-plot[filename]: plot packets in <filename>.
    The <filename> may be built out of $from (host and port), $fromHost, 
    $fromPort, $from[0..n] for the segments of the domain name. '$from.".xplot"'
    would be abc.def.com:1234. The corresponding fields exist for the to field.
    default: '$from[0].":".$fromPort."-".$to[0].":".$toPort.".xplot"'.
-list[filename]: output the list of generated plot files to filename.
-r: relative sequence numbers.
-t: time convert - insure that time is in decimal number of seconds.
-q: quiet - no visible output.
-?/-help: this message.
END_OF_USAGE
}

sub ReadArg
{
    local($arg) = substr(@_[0], 1);
    return 0 if ($arg eq 'e');
    if ($arg eq '?' || $arg eq 'help') {
	&usage;
	exit(0);
    } elsif ($arg eq 'w') {
	$Plotwindow = 1;
    } elsif ($arg eq 's') {
	$BreakOnSyns = 1;
    } elsif ($arg =~ /^f(\d*)/) {
	$EndOnFins = 1;
	$FinThreshold = $1 if ($1 ne '');
    } elsif ($arg eq 'c') {
	$Cumulative = 1;
    } elsif (substr($arg, 0, 4) eq 'plot') {
        $PlotTemplate = substr($arg, 4);
    } elsif (substr($arg, 0, 4) eq 'list') {
        $ListFile = substr($arg, 4);
	open($ListFile, ">$ListFile") || die "error opening \"$ListFile\" for writing: $!\n";
    } elsif ($arg eq 't') {
	$TimeConvert = 1;
    } elsif ($arg eq 'r') {
	$ForceRelative = 1;
    } elsif ($arg eq 'q') {
	$Quiet = 1;
    } elsif ($arg eq 'z') {
	$GzipOutput = 1;
    } else {
	&usage();
        print "unknown argument \"$arg\".\n";
    }
    return 1;
}

sub justSeconds
{
    local($time) = @_;
    return $time if (!($time =~ /:/));
    local($hr, $mn, $sec) = split(':', $time, 3);
    return $hr*3600 + $mn*60 + $sec;
}

sub subTimes
{
    local($big, $little) = @_;
    $big = &justSeconds($big);
    $little = &justSeconds($little);
    return ($big - $little);
}

sub newConversation
{
    local($from, $to, $time) = @_;

    # create variables for PlotTemplate
    $from =~ /(.*)\.(\d+)$/;
    local($fromHost, $fromPort) = ($1, $2);
    local(@from) = split('\.', $fromHost);
    $to =~ /(.*)\.(\d+)$/;
    local($toHost, $toPort) = ($1, $2);
    local(@to) = split('\.', $toHost);

    # create Xplot file from template
    $XplotName{$from} = eval($PlotTemplate);
    if ($GzipOutput) {
	open($from, "|gzip >$XplotName{$from}.gz") || die "error opening gzip pipe  to \"$from\".gz for writing: $!\n";
    } else { 
	open($from, ">$XplotName{$from}") || die "error opening \"$from\" for writing: $!\n";
    }
    print $from "timeval signed\ntitle\n$from-->$to\n";

    # initialize conversation
    $To{$from} = $to;
    $LastSendSeq{$from.'-'.$to} = -1;
    $StartTime{$from} = $time;
    push(@Froms, $from);
    $AckOffset{$from} = $SeqOffset{$from} = 0;
    $Ignored{$from.'-'.$to} = 0;
}

sub closeOut
{
    local($from) = @_;
    print $from "go\n";
    $TotalPackets += $Packets{$from};
    $TotalBytes += $Bytes{$from};
    print "$from: $Packets{$from} packets $Bytes{$from} bytes took ", 
	&subTimes($LastTime{$from}, $StartTime{$from}), "\n" if (!Quiet);
    $Mode{$from} = 'client' if (!defined($Mode{$from})); # added to handle bogus NT netmon dumps
    print $ListFile "$from $To{$from} $Mode{$from} $XplotName{$from} $Packets{$from} $Bytes{$from} ", 
	&subTimes($LastTime{$from}, $StartTime{$from}), ' ', $Bytes{$from}/($Bytes{$from} + $Packets{$from}*40), "\n" if $ListFile;
    $MaxLast = $LastTime{$from} if (!defined($MaxLast) || $LastTime{$from} > $MaxLast);
    $MinFirst = $StartTime{$from} if (!defined($MinFirst) || $StartTime{$from} < $MinFirst);
}

sub clearOut
{
    local($from, $to) = @_;

    # these attributes are specific to a conversation
    delete $LastSendSeq{$from.'-'.$to};
    delete $FirstSeq{$from.'-'.$to};
    delete $LastAckTime{$from.'-'.$to};
    delete $LastAckSeq{$from.'-'.$to};
    delete $LastWind{$from.'-'.$to};
    delete $WScale{$from.'-'.$to};

    # these stats are aggregated for all converstaions on a server
    delete $Packets{$from};
    delete $Bytes{$from};
    delete $LastTime{$from};
    delete $StartTime{$from};
    delete $Mode{$from};
    delete $Served{$from};	# not needed now because server is never cleared
    delete $To{$from};
    delete $XplotName{$from};
    delete $Ignored{$from.'-'.$to};
}

sub removeFrom
{
    local($from) = @_;
    local($i);
    foreach $i ($[ .. $#Froms) {
	if ($Froms[$i] eq $from) {
	    splice(@Froms, $i, 1);
	    return 1;
	}
    }
    return 0;
}

while (@ARGV[0] =~ /^-/ && &ReadArg(shift(@ARGV))) {}
if (($inputFile = shift(@ARGV))) {
    if ($inputFile =~ /.*\.gz$/) {
	open (TCPDUMP, "zcat $inputFile|") || die "error opening \"$inputFile\" for reading: $!\n";	
    } else {
	open (TCPDUMP, "<$inputFile") || die "error opening \"$inputFile\" for reading: $!\n";
    }
    $Tcpdump = 'TCPDUMP';
}

for ($lineNo = 1; <$Tcpdump>; $lineNo++) {
    local($i);
    local($ackIsZero) = (0);

    local($opts);
    local(@opts);
    local(%opts);

    chop;
    $opts = $_;   # save this for later 

    split(/ /);
    $time = $_[0];
    $from = $_[1];
    $to = $_[3];
    chop($to);			# strip off colon
    $flags = $_[4];

    last if (/\d+\s+packets/);
    if ($_[2] ne '>') {
	print stderr "tcpdump2xplot: Malformed entry in dump file $inputFile:$lineNo \"$_\"\n";
	next;
    }
    $time = &justSeconds($time) if ($TimeConvert);
    &newConversation($from, $to, $time) if (!(defined($StartTime{$from})));

    $opts =~ s/^.*<//;
    $opts =~ s/>.*//;
    @opts = split(/,/,$opts);

    for ($i = 0; $i <= $#opts; $i++) {
	local(@opt);
	$opts[$i] =~ s/^ sack/sack/;  # some tcpdumps put a stray space at the beginning of "sack" 
	@opt = split(/ /,$opts[$i]);
	if ($opt[0] =~ /nop/ || $opt[0] =~ /eol/) {
	    next;
	}
	$opts{$opt[0]} = join(' ', @opt[ 1 .. $#opt ]);
    }

    if ($flags =~ /S/) {
	if ($Mode{$from} eq 'client') {		# re-used client socket's syn
	    if ($BreakOnSyns) {
		&closeOut($from);
		&removeFrom($from);
		&clearOut($from, $to);
		&newConversation($from, $to, $time);
		$Mode{$from} = 'client';
	    }
	} elsif ($Mode{$from} eq 'server') {	# server's syn
	    $Served{$from}++;
	} else {				# client socket's syn
	    $Mode{$from} = 'client';
	    $Mode{$to} = 'server';
	}

	if (exists $opts{'wscale'}) {
	    local($val) = $opts{'wscale'};

	    if ($val < 15 && $val >= 0) {
		$WScale{$from.'-'.$to} = $val;
		print stderr "tcpdump2xplot: noticed wscale value for $from.-.$to of ";
		print stderr $WScale{$from.'-'.$to};
		print stderr ".\n";
	    } else {
		print stderr "tcpdump2xplot: ignoring wild wscale value \"$val\" in $inputFile:$lineNo\n";
	    }
	}
    }

    if ($flags =~ /F/ && $Mode{$from} eq 'client' && $EndOnFins && !$Ignored{$from.'-'.$to}) {
	local ($dif) = $time - $LastTime{$from};
	if ($dif > $FinThreshold) {
	    print stderr "tcpdump2xplot: delayed F ($dif seconds) in dump file $inputFile:$lineNo \"$_\"\n";
	}
	$Ignored{$from.'-'.$to} = 1;
    }

    next if ($Ignored{$from.'-'.$to} || $Ignored{$to.'-'.$from});
    $LastTime{$from} = $time;
    $Packets{$from}++;

    if (/$flags ([0-9]*):([0-9]*)\(([0-9]*)\) /) {

	local($f);
	local($t);
	local($n);

	$f = $1;
	$t = $2;
	$n = $3;

	if ($flags =~ /S/) {
	    $t++;
	    $n++;
	}

	if ($flags =~ /F/) {
	    $t++;
	    $n++;
	}

	$sendseq = $f;
	$LastSendSeq{$from.'-'.$to} = $sendseqlast = $t;
	$datalength = $n;
	$Bytes{$from} += $n;

	if (!defined $FirstSeq{$from.'-'.$to}) {
	    if ($ForceRelative) {
		$sendseq = 0;
		$LastSendSeq{$from.'-'.$to} = $sendseqlast = $t - $f;
		$FirstSeq{$from.'-'.$to} = 0;
		$ackIsZero = 1;
	    } else {
		$FirstSeq{$from.'-'.$to} = $f;
	    }
	}
    } else {
	$sendseq = $LastSendSeq{$from.'-'.$to};
	$sendseqlast = $LastSendSeq{$from.'-'.$to};
    }

    if (/win ([0-9]*)/) {	# remove space in "]*) /" to handle win <num><EOL> - EGP
	$window = $1;

	if (exists $WScale{$from.'-'.$to}
	    && !($flags =~ /S/)) {

	    $window <<= $WScale{$from.'-'.$to};
	}
    } else {
	$window = -1;
    }

    if (/ack ([0-9]*) /) {
	$ack = $ackIsZero ? 0 : $1;
    } else {
	$ack = -1;
    }

    if ($Cumulative) {
	# adjust sequence number to be relative to last ack.
	$sendseqlast -= $sendseq - $SeqOffset{$from};
	$sendseq = $SeqOffset{$from};
	$SeqOffset{$from} = $sendseqlast;
    } else {
	# adjust sequence number to be relative to start of conversation.
	$sendseq -= $FirstSeq{$from.'-'.$to};
	$sendseqlast -= $FirstSeq{$from.'-'.$to};
    }
    print $from "darrow $time $sendseq\n";
    print $from "uarrow $time $sendseqlast\n";
    print $from "line $time $sendseq $time $sendseqlast\n";

    if ($ack != -1) {
	$winend = $ack + $window;
	if (defined $LastAckTime{$to.'-'.$from}) {
	    local($lastAckSeq, $ackSeq) = ($LastAckSeq{$to.'-'.$from}, $ack);
	    if ($Cumulative) {
		# adjust sequence number to be relative to last ack.
		$ackSeq -= $lastAckSeq - $AckOffset{$to};
		$lastAckSeq = $AckOffset{$to};
		$AckOffset{$to} = $ackSeq;
	    } else {
		# adjust sequence number to be relative to start of conversation.
		$lastAckSeq -= $FirstSeq{$to.'-'.$from};
		$ackSeq -= $FirstSeq{$to.'-'.$from};
	    }

	    print $to "line $LastAckTime{$to.'-'.$from} $lastAckSeq ";
	    print $to "$time $lastAckSeq\n";
	    if ($LastAckSeq{$to.'-'.$from} != $ack) {
		print $to "line $time $lastAckSeq $time $ackSeq\n";
	    } else {
		print $to "dtick $time $ackSeq\n";
	    }

	    if (exists $opts{'sack'}) {
		local($sacks) = $opts{'sack'};
		local(@sacks);
		local($i);

                # munge newer tcpdump format for SACK so that it looks
                # just just like my old format

		$sacks =~ s/\{/ /g;
		$sacks =~ s/\}/ /g;

		# print stderr "SACKS before: $sacks \n";

		$sacks =~ s/^sack [123]  //;
		$sacks =~ s/^[123]  //;

		# print stderr "SACKS after : $sacks \n";

		@sacks = split(/ /, $sacks);
		
		for ($i = 0; $i <= $#sacks; $i++) {
		    local($start);
		    local($end);

		    $sacks[$i] =~ /([0-9]*):([0-9]*)/;
		    $start = $1;
		    $end = $2;

		    # print stderr "SACK start $start end $end \n";

		    if ($Cumulative) {
			# yikes! what to do?
		    } else {
			# adjust sequence number to be relative to start of conversation.
			# print stderr "SACK relative to $FirstSeq{$to.'-'.$from} \n";

			$start -= $FirstSeq{$to.'-'.$from};
			$end -= $FirstSeq{$to.'-'.$from};

			# print stderr "SACK line $time $start $time $end green\n";
			print $to "line $time $start $time $end green\n";

		    }
		}
	    }

	    if ($Plotwindow) {
		local($lastWinSeq, $winSeq) = ($LastWind{$to.'-'.$from}, $winend);
		if ($Cumulative) {
		    # adjust sequence number to be relative to last ack.
		    $winSeq -= $lastWinSeq - $AckOffset{$to};
		    $lastWinSeq = $AckOffset{$to};
		} else {
		    # adjust sequence number to be relative to start of conversation.
		    $lastWinSeq -= $FirstSeq{$to.'-'.$from};
		    $winSeq -= $FirstSeq{$to.'-'.$from};
		}

		print $to "line $LastAckTime{$to.'-'.$from} $lastWinSeq ";
		print $to "$time $lastWinSeq\n";
		if ($LastWind{$to.'-'.$from} != $winend) {
		    print $to "line $time $lastWinSeq $time $winSeq\n";
		} else {
		    print $to "utick $time $winSeq\n";
		}
	    }
	}
	$LastAckTime{$to.'-'.$from} = $time;
	$LastAckSeq{$to.'-'.$from} = $ack;
	$LastWind{$to.'-'.$from} = $winend;
    }
}

foreach $from (@Froms) {
    &closeOut($from);
}
print "summary: $TotalPackets packets $TotalBytes bytes took ", &subTimes($MaxLast, $MinFirst), 
    " efficiency: ", $TotalBytes/($TotalBytes + $TotalPackets*40), "\n" if (!Quiet);

print $ListFile "summary: $TotalPackets $TotalBytes ",
    &subTimes($MaxLast, $MinFirst), " ", $TotalBytes/($TotalBytes + $TotalPackets*40), "\n" if ($ListFile);

close($ListFile) if ($ListFile);

exit 0;

