#!/usr/bin/perl

use strict;
use warnings;
use Socket;
use lib "./blib/lib";
use Net::OSCAR qw(:all);
use Net::OSCAR::XML;
use Net::OSCAR::Utility qw(hexdump);
use Net::OSCAR::Constants;
use Net::Pcap;

our $session = Net::OSCAR->new();
our $init_time = undef;

sub BEGIN {
	eval {
		require "net/bpf.ph";
	};
	die "Couldn't find net/bpf.ph.\nPlease create it by doing cd /usr/include ; h2ph net/bpf.h\n$@\n" if $@;
}

my $file = shift or die "Usage: snacsnatcher pcapfile\n";

# Quick and dirty protocol analyzer

use vars qw($packet %buffer %bufflen %snacbuff %ft_states %seqnos $datalink @blarray);
$packet = 0;

sub ssdump_scalar($$);
sub ssdump_list($$);
sub ssdump_hash($$);

sub ssdump_scalar($$) {
	my($val, $depth) = @_;

	my $hex = hexdump($val);
	if($hex and $hex ne $val) {
		print join("\n",
			map {
				("\t" x $depth) . $_
			} split(/\n/,
				$hex
			)
		), "\n";
	} else {
		$val ||= "";
		print "$val\n";
	}
}

sub ssdump_list($$) {
	my($val, $depth) = @_;

	print "\t" x $depth;
	foreach (@$val) {
		print "[\n";

		if(!ref($_)) {
			print "\t" x ($depth+1);
			ssdump_scalar($_, $depth);
		} elsif(ref($_) eq "HASH") {
			ssdump_hash($_, $depth+1);
		} elsif(ref($_) eq "ARRAY") {
			ssdump_list($_, $depth+1);
		} elsif(ref($_) eq "SCALAR") {
			print "\t" x ($depth+1);
			ssdump_scalar($$_, $depth+1);
		} else {
			die "Unknown reftype: " . ref($_) . "\n";
		}

		print "\t" x $depth;
		print "],";
	}
	print "\n";
}

sub ssdump_hash($$) {
	my($struct, $depth) = @_;

	foreach my $key (sort keys %$struct) {
		my $val = $struct->{$key};

		print "\t" x $depth;
		print $key, " => ";

		if(!ref($val)) {
			if($key =~ /ip$/ and $val =~ /^\d+$/) {
				my($q1, $q2, $q3, $q4) = (
					($val >> 24),
					(($val >> 16) & 0xFF),
					(($val >> 8) & 0xFF),
					($val & 0xFF)
				);
				$val = "$q1.$q2.$q3.$q4";
			} elsif($key eq "capability") {
				$val = OSCAR_CAPS_INVERSE()->{$val} if exists(OSCAR_CAPS_INVERSE()->{$val});
			}

			ssdump_scalar($val, $depth);
		} elsif(ref($val) eq "HASH") {
			print "\n";
			ssdump_hash($val, $depth+1);
		} elsif(ref($val) eq "ARRAY") {
			print "\n";

			if($key eq "capabilities") {
				@$val = map {
					exists(OSCAR_CAPS_INVERSE()->{$_}) ?
					OSCAR_CAPS_INVERSE()->{$_} :
					$_
				} @$val;
			} elsif($key eq "shortcaps") {
				@$val = map {
					exists(OSCAR_CAPS_SHORT_INVERSE()->{$_}) ?
					OSCAR_CAPS_SHORT_INVERSE()->{$_} :
					$_
				} @$val;
			}

			ssdump_list($val, $depth);
		} elsif(ref($val) eq "SCALAR") {

			ssdump_scalar($$val, $depth);
		} else {
			die "Unknown reftype: " . ref($val) . "\n";
		}
	}
}

sub got_packet($$$) {
	my($user, $hdr, $pkt) = @_;
	my($inaddr, $outaddr);
	my $tlv;

	my $time = $hdr->{tv_sec} . "." . $hdr->{tv_usec};
	$init_time ||= $time;
	$time -= $init_time;
	$time = sprintf("%0.3f", $time);

	$packet++;
	# This removes the datalink-level headers from a packet.
	# You may need to adjust this - this is a very Q&D hack.
	# Only ethernet (DLT_EN10MB) is tested.
	#
	# These are taken from tcpdump.
	#
	if($datalink == DLT_NULL or $datalink == DLT_LOOP) {
		substr($pkt, 0, 4) = "";
	} elsif($datalink == DLT_EN10MB or $datalink == DLT_IEEE802) {
		substr($pkt, 0, 14) = "";
	} elsif($datalink == DLT_SLIP) {
		substr($pkt, 0, 16) = "";
	} elsif($datalink == DLT_PPP) {
		substr($pkt, 0, 4) = "";
	} elsif($datalink == DLT_LINUX_SLL) {
		substr($pkt, 0, 16) = "";
	} else {
		die "Unsupported datalink $datalink\n";
	}

	my($iplen) = unpack("C", $pkt);
	$iplen = ($iplen&0xF) * 4;
	my $src = substr($pkt, 12, 4);
	my $dst = substr($pkt, 16, 4);
	substr($pkt, 0, $iplen) = ""; #Get rid of IP headers
	$src = inet_ntoa($src);
	$dst = inet_ntoa($dst);

	my($src_port, $dst_port, $seqno, $ack_seq, $tcplen, $flags) = 
		unpack("nnNNCC", $pkt);
	$tcplen = ($tcplen>>4)*4;
	substr($pkt, 0, $tcplen) = "";

	return if $flags & 0x2; # SYN
	return unless $flags & 0x8; # PSH

	my $conn_key = "$src:$src_port -> $dst:$dst_port";
	$buffer{$conn_key} ||= "";
	$bufflen{$conn_key} ||= 0;


	# Ignore retransmissions
	$seqnos{$conn_key} ||= [undef, undef, undef, undef, undef, undef, undef, undef, undef, undef];
	return if grep {defined($_) and $_ eq $seqno} @{$seqnos{$conn_key}};
	shift @{$seqnos{$conn_key}};
	push @{$seqnos{$conn_key}}, $seqno;	

	PACKET: while($pkt) {
		if($buffer{$conn_key}) {
			$pkt = $buffer{$conn_key} . $pkt;
			$buffer{$conn_key} = "";
		}

		if($bufflen{$conn_key}) {
			if(length($pkt) < $bufflen{$conn_key}) {
				$buffer{$conn_key} = $pkt;
				return;
			} else {
				$bufflen{$conn_key} = 0;
			}
		} else {
			if(length($pkt) < $tcplen) {
				$buffer{$conn_key} = $pkt;
				$bufflen{$conn_key} = $tcplen;
				return;
			}
		}

		if($snacbuff{$conn_key}) {
			$pkt = $snacbuff{$conn_key} . $pkt;
			$snacbuff{$conn_key} = "";
		}

		if(substr($pkt, 0, 4) eq "OFT2") {
			process_xfer($time, \$pkt, $conn_key);
		} elsif(substr($pkt, 0, 1) eq "*") {
			process_snac($time, \$pkt, $conn_key);
		} else {
			if($ft_states{$conn_key}) {
				print "$time: $conn_key: " . length($pkt) . " bytes of FT data\n";
				#print hexdump($pkt), "\n";
			}

			$pkt = "";		
		}
	}
}

sub process_xfer {
	my($time, $pkt, $conn_key) = @_;

	print "$time: $conn_key\n";
	$ft_states{$conn_key} = 1;

	my %ft_data = protoparse($session, "file_transfer_header")->unpack($$pkt);
	printf "\t[type=%04X] [encrypt=%d] [compress=%d] [files=%d/%d] [parts=%d/%d] [bytes=%d/%d]\n",
		delete @ft_data{qw(type encrypt compress files_left file_count parts_left part_count bytes_left byte_count)};

	print "\tHEADER IS NOT 256 BYTES!!\n" unless $ft_data{header_length} == 256;
	substr($$pkt, 0, $ft_data{header_length} + 4) = "";

	ssdump_hash(\%ft_data, 1);
	print "\n";
}

sub process_snac {
	my($time, $pkt, $conn_key) = @_;

	my($chan, $seqno, $len) = unpack("xCnn", substr($$pkt, 0, 6, ""));
	if(length($$pkt) < $len) {
		$snacbuff{$conn_key} = pack("CCnn", 42, $chan, $seqno, $len);
		$snacbuff{$conn_key} .= $$pkt;
		return;
	}
	my $snac = substr($$pkt, 0, $len, "");

	print "$time: $conn_key";
	printf " ch=%02X", $chan;

	my %snac_data = protoparse($session, "snac")->unpack($snac);
	printf " fl=%02X/%02X", $snac_data{flags1} || 0, $snac_data{flags2} || 0;
	printf " [%04X/%04X]", $snac_data{family} || 0, $snac_data{subtype} || 0;

	my $protobit = snac_to_protobit(%snac_data);
	if(!$protobit) {
		print " == UNKNOWN";
		print hexdump($snac_data{data}, 1);
		print "\n";
	} else {
		print " == $protobit\n";
		my %data = protoparse($session, $protobit)->unpack($snac_data{data});
		if($protobit =~ /^buddylist_(add|modify|delete)$/) {
			%data = protoparse($session, "buddylist_change")->unpack($snac_data{data});
		}

		if($protobit =~ /^(incoming|outgoing)_IM$/) {
			my $channel_data;

			if($data{channel} == 1) {
				$channel_data = {protoparse($session, "standard_IM_footer")->unpack($data{message_body})};
			} elsif($data{channel} == 2) {
				$channel_data = {protoparse($session, "rendezvous_IM")->unpack($data{message_body})};
				my $type = OSCAR_CAPS_INVERSE()->{$channel_data->{capability}};

				if($type eq "chat") {
					$channel_data->{svcdata} = {protoparse($session, "chat_invite_rendezvous_data")->unpack($channel_data->{svcdata})};
				} elsif($type eq "filexfer") {
					$channel_data->{svcdata} = {protoparse($session, "file_transfer_rendezvous_data")->unpack($channel_data->{svcdata})};
				} elsif($type eq "sendlist") {
					$channel_data->{svcdata} = {protoparse($session, "buddy_list_transfer_rendezvous_data")->unpack($channel_data->{svcdata})};
				}
			} else {
				$channel_data = $data{message_body};
			}

			$data{message_body} = $channel_data;
		}

		ssdump_hash(\%data, 1);
	}

	print "\n";
}

my $pcap = Net::Pcap::open_offline($file, \$!) or die "Couldn't open $file: $!\n";
$datalink = Net::Pcap::datalink($pcap);
Net::Pcap::dispatch($pcap, 0, \&got_packet, undef);
Net::Pcap::close($pcap);
