#!/usr/bin/perl 

#########################################################################################
# Copyright (C) 2010 Leon Ward 
# client.pl - Part of the OpenFPC - (Full Packet Capture) project
#
# Contact: leon@rm-rf.co.uk
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#########################################################################################

use strict;
use warnings;
use Data::Dumper;
use IO::Socket::INET;
use OFPC::Request; 
use OFPC::Parse;
use Getopt::Long;
use Switch;
use Digest::MD5(qw(md5_hex));
use Term::ReadKey;

my $now=time();
my $openfpcver="0.6";
my $timeoffset=0;		# Default time offset if a --timstamp isn't specified 
my $debug;
my (%config,$verbose);

# Hint: "ofpc-v1 type:event sip:192.168.222.1 dip:192.168.222.130 dpt:22 proto:tcp timestamp:1274864808 msg:Some freeform text";
my %cmdargs=( user => 0,
	 	password => 0, 
		server => "localhost",
		port => "4242",
		action => "fetch",
		logtype => "auto",
		filetype => "PCAP",
		debug => 0,
		verbose => 0,
		filename => "/tmp/pcap-openfpc-$now",
		logline => 0,
		quiet => 0,
		gui => 0,
		sumtype =>0,
		last => 0,
		);

my %request=(	user => 0,
		password => 0,
		action => 0,
		device => 0,
		logtype => 0,
		filetype => 0,
		logline => 0,
		sip => 0,
		dip => 0,
		spt => 0,
		dpt => 0,
		bpf => 0,
		proto => 0,
		timestamp => 0,
		stime => 0,
		etime => 0,
		comment => 0,
		sumtype => 0,
		dbsave => 0,
		);

my %result=(
		success => 0,
		filename => 0,
		position => 0,
		md5 => 0,
		expected_md5 => 0,
		message => 0,
		size => 0,
	);

sub showhelp{
	print <<EOF 
  ./openfpc-client <options>

  --------   General   -------
  --server or -s <openfpc server IP>	OpenFPC server to connect to
  --port or -o <TCP PORT>		Port to connect to (default 4242)
  --user or -u <username>		Username	
  --password or -p <password>		Password (if not supplied, it will be prompted for)
  --action or -a <action>		OFPC action to take. fetch, store, status, summary
  --verbose or -v 			Run in verbose mode
  --debug or -d				Run in debug mode (very verbose)
  --write or -w				Output PCAP file to write to
  --quiet or -q				Quiet, shhhhhhh please. Only print saved filename||error
  --gui	or -g				Output that's parseable via OpenFPC's gui (or other tool)
  --comment or -m 			Comment for session
  --device 				OpenFPC Node to extract from (via OpenFPC-Proxy --server)

  -------- Traffic Constraints -------
  --bpf					Specify constraints with a BPF syntax
  --logline or -e <line>		Logline, must be supported by OFPC::Parse
  --src-addr <host>			Source IP
  --dst-addr <host>			Destination IP
  --src-port <port>			Source Port
  --dst-port <port>			Destination Port
  --vlan <vlan>				VLAN (NOT DONE YET)

  -------- Connection Tables -------
  --summarytype <summary type>		Generate a summary 
  --save				Save connection data in local database
  --top <number>			Show top x entries
 
  -------- Time Constraints -------
  --last <seconds>			Easy to specify relative time range to \$now
  --timestamp	<timestamp>		Event timestamp
  --eachway <count>			Expand timestamp over extra files (NOT DONE YET)
  --stime				Start timestamp
  --etime                               End timestamp

EOF
}


=head2 readrcfile
	Read in an optional rc file found in a couple of default locations.
	This is to prevent a user from re-typing the same config options
	like --user, --server etc

	Takes, \%cmdargs,
	Return \%cmdargs,

	The return has some alternative values set. This way the command-line
	options override those in the rc file.
	-Leon
=cut
sub readrcfile{
	my $config=shift;

	my @rcfiles=("./openfpc-client.rc",			# CWD
			"$ENV{HOME}/.openfpc-client.rc",			# Personal
			"/etc/openfpc/openfpc-client.rc",	# System default
		);	
	my $rcfile=0;

	foreach my $file (@rcfiles) {
		if (-e $file) {
			print "* Reading configuration from $file\n";
			$rcfile=$file;
			last;
		}
	}

	if ($rcfile) {
		unless (open(RC, '<', "$rcfile")){
			return($config);
			print "* Error, unable to open rc file $rcfile";
		} else {
			while(<RC>) {
				chomp;
		        	if ( $_ =~ m/^[a-zA-Z]/) {
 		 	               (my $key, my $value) = split /=/, $_;
					# If config line looks valid, set it
					if (defined $config->{$key}) {
						$config->{$key} = $value;
					} else {
						print "* Invalid variable $key found in $rcfile \n";
					}
				}
			}
		}
	}
	return($config);
}

=head2 convbytes
	Convert a number of bytes into MB or GB
=cut
sub convbytes{
	my $bytes=shift;
	my $units="Bytes";
	if ($bytes =~ /\d+/) {
		if ($bytes >=1000000 ) {
			$bytes = sprintf( "%0.2f", $bytes/1000000 );
			$units = "GB";
		} elsif ( $bytes >= 1000000 ) {
			$bytes = sprintf( "%0.2f", $bytes/1000000 );
			$units = "MB";
		} 
	}
	return("$bytes $units");
}

sub displayResult{
	# TODO, why the hell is $result a global?
	# How did I get in to that state: Need to fix this.

	if ($result{'success'} == 1) { 			# Request is Okay and being processed
		unless ($cmdargs{'gui'}) {  		# Command line output
			if ($request{'action'} eq "fetch") {	
				print 	"#####################################\n" .
					"Date    : " . localtime($now) . "\n" .
					"Filename: $result{'filename'} \n" .
					"Size    : $result{'size'}\n" .
					"MD5     : $result{'md5'}\n";
			} elsif ($request{'action'} eq "store") {
				print 	"#####################################\n" .
				 	"Queue Position: $result{'position'} \n".
					"Remote File   : $result{'filename'}\n" .
					"Result        : $result{'message'}\n";
			} elsif ($request{'action'} eq "status" ) {
				print 	"####################################\n" .
					" OpenFPC Node name   :  $result{'nodename'}\n".
					" OpenFPC Node Type   :  $result{'ofpctype'} \n".
					" OpenFPC Version     :  $result{'version'} \n".
					" Oldest Packet       :  $result{'firstpacket'} (" . localtime($result{'firstpacket'}) .")\n".
					" Oldest Session      :  $result{'firstctx'} (" . localtime($result{'firstctx'}) .")\n".
					" Packet utilization  :  $result{'packetspace'}\% \n" .
					" Session utilization :  $result{'sessionspace'}\% \n" .
					" Session DB Size     :  $result{'sessioncount'} rows \n" .
					" Session lag         :  $result{'sessionlag'} files \n" .
					" Storage utilization :  $result{'savespace'}\% \n" .
					" Packet space used   :  $result{'packetused'} (" . convbytes($result{'packetused'}) . ")\n" .
					" Session space used  :  $result{'sessionused'} (" . convbytes($result{'sessionused'}) . ")\n" .
					" Storage used        :  $result{'saveused'} (" . convbytes($result{'saveused'}) . ")\n" .
					" Load avg 1          :  $result{'ld1'} \n" .
					" Load avg 5          :  $result{'ld5'} \n" .
					" Load avg 15         :  $result{'ld15'} \n" .
					" Errors              :  $result{'message'} \n";
			} elsif ($request{'action'} eq "summary") {
				print 	"#####################################\n" .
					" Summary Table       :  $request{'sumtype'}\n" .
					" Time Range          :  " . localtime($request{'stime'}) . " -> " . localtime($request{'etime'}) . "\n" .
					"#####################################\n";
				my $table=$result{'table'};

                                foreach my $foo (@$table) {
                                	foreach (@$foo) {
                                		printf '%15s', "$_";
                                	}
                                }
			} else {
				die("Results: Unknown action: $request{'action'}\nSorry I don't know how to display this data.");
			}
		} else {	
			# GUI firendly output
			# Provide output that is easy to parse
			# result=0   	Fail
			# result=1	success
			# result,action,filename,size,md5,expected_md5,position,message

			print "1,$request{'action'},$result{'filename'},$result{'size'},$result{'md5'},$result{'expected_md5'}," .
				"$result{'position'},$result{'message'}\n";
		}
	} else {				# Problem with request, provide fail info
		if ($cmdargs{'gui'}) {
			print "0,$request{'action'},$result{'filename'},$result{'size'},$result{'md5'},$result{'expected_md5'}," .
				"$result{'position'},$result{'message'}\n";
		} else {
			print "Problem processing request: $result{'message'}\n";
			print "Expected: $result{'expected_md5'}\n" if ($result{'expected_md5'});
			print "Got     : $result{'md5'}\n" if ($result{'md5'});
		}
	}
}


# Read in defailts from openfpc-client.rc if discovered
my $tempref=readrcfile(\%cmdargs);
%cmdargs=%$tempref;

GetOptions (    'u|user=s' => \$cmdargs{'user'},
		's|server=s' => \$cmdargs{'server'},
		'o|port=s' => \$cmdargs{'port'},
		'd|debug' => \$cmdargs{'debug'},
		'h|help' => \$cmdargs{'help'},
		'q|quiet' => \$cmdargs{'quiet'},
		'w|write=s'=> \$cmdargs{'filename'},
		'v|verbose' => \$cmdargs{'verbose'},
		't|logtype=s' => \$cmdargs{'logtype'},
		'e|logline=s' => \$cmdargs{'logline'},
		'a|action=s' => \$cmdargs{'action'},
		'p|password=s' => \$cmdargs{'password'},
		'm|comment=s' => \$cmdargs{'comment'},
		'g|gui'	=> \$cmdargs{'gui'},
		'z|zip' => \$cmdargs{'zip'},
		't|time|timestamp=s' => \$cmdargs{'timestamp'},
		'src-addr=s' => \$cmdargs{'sip'},
                'dst-addr=s' => \$cmdargs{'dip'}, 
                'src-port=s' => \$cmdargs{'spt'},
                'dst-port=s' => \$cmdargs{'dpt'},
                'proto=s' => \$cmdargs{'proto'},
		'device=s' => \$cmdargs{'device'},
		'stime=s' =>  \$cmdargs{'stime'},
		'etime=s' => \$cmdargs{'etime'},
		'bpf=s' => \$cmdargs{'bpf'},
		'sumtype|summarytype=s' => \$cmdargs{'sumtype'},
		'save' => \$cmdargs{'dbsave'},
		'l|last=s' => \$cmdargs{'last'},
                );



# Need to tidy this up.
$request{'user'} 	= $cmdargs{'user'}	if $cmdargs{'user'};
$config{'server'} 	= $cmdargs{'server'}	if $cmdargs{'server'}; 
$config{'port'} 	= $cmdargs{'port'}	if $cmdargs{'port'};
$request{'filename'} 	= $cmdargs{'filename'}	if $cmdargs{'filename'};
$request{'logtype'} 	= $cmdargs{'logtype'}   if $cmdargs{'logtype'};
$request{'action'} 	= $cmdargs{'action'}	if $cmdargs{'action'};
$request{'logline'} 	= $cmdargs{'logline'}	if $cmdargs{'logline'};
$request{'password'} 	= $cmdargs{'password'}  if $cmdargs{'password'};
$request{'comment'} 	= $cmdargs{'comment'}   if $cmdargs{'comment'};
$request{'device'} 	= $cmdargs{'device'}	if $cmdargs{'device'};
$request{'filetype'} 	= "ZIP"			if $cmdargs{'zip'};
$request{'bpf'}		= $cmdargs{'bpf'}	if $cmdargs{'bpf'};
$request{'stime'} 	= $cmdargs{'stime'} 	if ($cmdargs{'stime'});
$request{'etime'} 	= $cmdargs{'etime'} 	if ($cmdargs{'etime'});
$request{'sumtype'}	= $cmdargs{'sumtype'}   if ($cmdargs{'sumtype'});
$request{'save'}	= $cmdargs{'save'}   	if ($cmdargs{'save'});

if ($cmdargs{'debug'}) { 
	$debug=1;
	$verbose=1;
}

if ($cmdargs{'last'}) {
	$request{'etime'} = $now;
	$request{'stime'} = $now - $cmdargs{'last'};
	$cmdargs{'etime'} = $now;
	$cmdargs{'stime'} = $now - $cmdargs{'last'};
}

if ($debug) {
	print "----Config----\n".
	"Server   :  $config{'server'}\n" .
	"Port     :  $config{'port'}\n" .
	"User     :  $request{'user'}\n" .
	"Action   :  $request{'action'}\n" .
	"Logtype  :  $request{'logtype'}\n" .
	"Logline  :  $request{'logline'}\n" .
	"Filename :  $cmdargs{'filename'}\n" .
	"SumType  :  $cmdargs{'sumtype'} \n" .
	"Last     :  $cmdargs{'last'}\n" .
	"stime    :  $request{'stime'} " . localtime($request{'stime'}) . "\n" .
	"etime    :  $request{'etime'} " . localtime($request{'etime'}) . "\n" .
	"\n";
}

# Provide a banner and queue position if were not in GUI or quiet mode
unless( ($cmdargs{'quiet'} or $cmdargs{'gui'})) {
	print "\n   * openfpc-client $openfpcver * \n   Part of the OpenFPC project\n\n" ;
	$request{'showposition'} = 1;
}

if ($cmdargs{'help'}) {
	showhelp;
	exit;
}

# Check we have enough constraints to make an extraction with.
if ($request{'action'} =~ m/(fetch|store)/)  {
	unless ($request{'logline'} or ($cmdargs{'bpf'} or $cmdargs{'sip'} or $cmdargs{'dip'} or $cmdargs{'spt'} or $cmdargs{'dpt'} )) {
		unless ($cmdargs{'gui'} )  {
			showhelp;
		} else {
			$result{'message'} = "Insufficient Constraints added. Please add some session identifiers";
			displayResult($cmdargs{'gui'});
			exit 1;
		}
		print "Error: This action requres a request line or session identifiers\n\n";
		exit;
	}
} elsif ($request{'action'} eq "status" ) {
	print "Sending status request\n" if ($debug);
} elsif ($request{'action'} eq "summary" ) {
	print "DEBUG: Fetching summary data\n" if ($debug);
} else {
	die("Action $request{'action'} invalid, or not implemented yet");
}

# If we are in GUI mode, PHP's escapecmd function could have broken out logline, lets unescape it

if ($cmdargs{'gui'}) {
	$request{'logline'} =~ s/\\(.)/$1/g;
}


# Convert session info into a "logline" to make a request.
unless ($cmdargs{'logline'}) {
	my $logline=OFPC::Parse::sessionToLogline(\%cmdargs);
	$request{'logline'} = $logline;	
	print "Logline created from session IDs: $request{'logline'}\n" if ($debug);
}

# Unless user has passed a user and password via cmdargs, lets request one.
unless ($request{'user'}) {
	print "Username: ";
	my $username=<STDIN>;
	chomp $username;
	$request{'user'} = $username;
}


unless ($request{'password'}) {
	print "Password for user $request{'user'} : ";
	ReadMode 'noecho';
	my $userpass = ReadLine 0;
	chomp $userpass;
	$request{'password'} = $userpass;
	ReadMode 'normal';
	print "\n";
}

my $sock = IO::Socket::INET->new(
				PeerAddr => $config{'server'},
                                PeerPort => $config{'port'},
                                Proto => 'tcp',
                                );  
unless ($sock) { 
	$result{'message'} = "Unable to create socket to server $config{'server'} on TCP:$config{'port'}\n"; 
	displayResult($cmdargs{'gui'});
	exit 1;
}

print "DEBUG: Connected to $config{'server'}\n" if ($debug);
%result=OFPC::Request::request($sock,\%request);
print "DEBUG: Sent Request\n" if ($debug);
close($sock);

displayResult($cmdargs{'gui'});

# provide output back to the user / gui
