#!/usr/bin/perl

use strict;
use warnings;
use Getopt::Long qw(:config posix_default bundling);
use Net::Inspect::Debug '%TRACE';
use Net::Inspect::L2::Pcap;
use Net::Inspect::L3::IP;
use Net::Inspect::L4::TCP;
use Net::Inspect::L4::UDP;
use Net::PcapWriter 0.7;
use Net::Pcap qw(pcap_open_offline pcap_loop);
use Net::IMP;
use Net::IMP::Cascade;
use Net::IMP::Debug;

# interface we support in this program
my @rtypes = (
    IMP_PASS,
    IMP_PREPASS,
    IMP_DENY,
    IMP_REPLACE,
    IMP_LOG,
    IMP_ACCTFIELD,
    IMP_PAUSE,
    IMP_CONTINUE,
);
my @interface = (
    [ IMP_DATA_STREAM, \@rtypes ],
    [ IMP_DATA_PACKET, \@rtypes ],
);


sub usage {
    print STDERR <<USAGE;

filter tcp connections from pcap file using Net::IMP analyzers
$0 Options*  -r in.pcap -w out.pcap

Options:
  -h|--help               show usage
  -M|--module mod[=arg]   use Net::IMP module for connections
			  can be given multiple times for cascading modules
  -r|--read  in.pcap      input pcap file
  -w|--write out.pcap     output pcap file
  -d|--debug              debug mode
  -T|--trace T            Net::Inspect traces

USAGE
    exit(2);
}

my (@module,$infile,$outfile);
GetOptions(
    'M|module=s' => \@module,
    'r|read=s'   => \$infile,
    'w|write=s'  => \$outfile,
    'h|help'     => sub { usage() },
    'd|debug'    => \$DEBUG,
    'T|trace=s'  => sub { $TRACE{$_}=1 for split(m/,/,$_[1]) }
);

$Net::Inspect::Debug::DEBUG=$DEBUG;

$infile ||= '/dev/stdin';
my $err;
my $pcap_in = pcap_open_offline($infile,\$err) or die $err;
my $pcap_out = Net::PcapWriter->new( $outfile || \*STDOUT ) or die $!;


my @factory;
for my $module (@module) {
    $module eq '=' and next;
    my ($mod,$args) = $module =~m{^([a-z][\w:]*)(?:=(.*))?$}i
	or die "invalid module $module";
    eval "require $mod" or die "cannot load $module";
    my %args = $mod->str2cfg($args//'');
    my $factory = $mod->new_factory(%args) or
	croak("cannot create Net::IMP factory for $mod");
    push @factory, $factory;
}

my $imp_factory;
if (@factory == 1) {
    $imp_factory = $factory[0];
} elsif (@factory) {
    $imp_factory = Net::IMP::Cascade->new_factory(
	parts => \@factory
    ) or croak("cannot create factory from Net::IMP::Cascade");
}
@interface = $imp_factory->get_interface(@interface) or
    croak("cannot use modules - wrong interface");

my @l4;
for my $if (@interface) {
    if ( $if->[0] == IMP_DATA_STREAM ) {
	# factory support stream interface
	my $cw  = ConnWriter->new($pcap_out,$imp_factory);
	push @l4,Net::Inspect::L4::TCP->new($cw);
    } elsif ( $if->[0] == IMP_DATA_PACKET ) {
	# factory support packet interface
	my $pw  = PacketWriter->new($pcap_out,$imp_factory);
	push @l4,Net::Inspect::L4::UDP->new($pw);
    }
}
my $raw = Net::Inspect::L3::IP->new(@l4>1 ? \@l4:@l4);
my $pc  = Net::Inspect::L2::Pcap->new($pcap_in,$raw);

my $time;
pcap_loop($pcap_in,-1,sub {
    my (undef,$hdr,$data) = @_;
    if ( ! $time || $hdr->{tv_sec}-$time>10 ) {
	$_->expire($time = $hdr->{tv_sec}) for(@l4);
    }
    return $pc->pktin($data,$hdr);
},undef);


package ConnWriter;
use base 'Net::IMP::Filter';
use Net::IMP;

sub new {
    my ($class,$pcap,$imp) = @_;
    my $self = $class->SUPER::new($imp, pcap => $pcap, expire => 0);
    return $self;
}

sub new_connection {
    my ($self,$meta) = @_;
    my $imp = $self->{imp}
	&& $self->{imp}->new_analyzer(meta => $meta);
    my $pcap = $self->{pcap}->tcp_conn(
	$meta->{saddr}, $meta->{sport},
	$meta->{daddr}, $meta->{dport},
    );
    return $self->new($pcap,$imp);
}

sub syn { return 1 }
sub fatal { warn "fatal: $_[1]\n" }
sub in {
    my ($self,$dir,$data,$eof) = @_;
    $self->SUPER::in($dir,$data,IMP_DATA_STREAM) if $data ne '';
    $self->SUPER::in($dir,'',IMP_DATA_STREAM) if $eof;
    return length($data);
}

sub out {
    my ($self,$dir,$data) = @_;
    $self->{pcap}->write($dir,$data);
}

sub expire {
    my ($self,$expire) = @_;
    return $self->{expire} && $time>$self->{expire};
}

sub log {
    my ($self,$level,$msg,$dir,$offset,$len) = @_;
    print STDERR "[$level] $msg\n";
}


package PacketWriter;
sub new {
    my ($class,$pcap,$imp) = @_;
    my $self = bless [ $imp,$pcap ],$class;
    return $self;
}

sub pktin {
    my ($self,$data,$meta) = @_;
    my $imp = $self->[0] && $self->[0]->new_analyzer(meta => $meta);
    my $pcap = $self->[1]->udp_conn(
	$meta->{saddr}, $meta->{sport},
	$meta->{daddr}, $meta->{dport},
    );
    return PacketWriter::Conn->new($pcap,$imp);
}

package PacketWriter::Conn;
use base 'Net::IMP::Filter';
use Net::IMP;

sub new {
    my ($class,$pcap,$imp) = @_;
    return $class->SUPER::new($imp, pcap => $pcap, expire => 0 );
}

sub fatal { warn "fatal: $_[1]\n" }
sub pktin {
    my ($self,$dir,$data) = @_;
    $self->SUPER::in($dir,$data,IMP_DATA_PACKET);
    return length($data);
}

sub out {
    my ($self,$dir,$data) = @_;
    $self->{pcap}->write($dir,$data) if $data ne '';
}

sub expire {
    my ($self,$expire) = @_;
    return $self->{expire} && $time>$self->{expire};
}

sub log {
    my ($self,$level,$msg,$dir,$offset,$len) = @_;
    print STDERR "[$level] $msg\n";
}

