#!/usr/bin/perl -w
###################################################################################
#
# nutop : top like interface to Nufw User Connection tracking.
#
# Copyright(C) 2003-2004 Eric Leblond <eric@regit.org>
#		     Vincent Deffontaines <vincent@gryzor.com>
#
# 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, version 2 of the License.
#
# 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 Getopt::Long;
use Time::localtime;
#Parsing works this way : very first, we parse the "-c" parameter which may
#specify a config file other than the default.
#then we look in config file
#Parameters are set from there
#Then we parse parameters on command line, and they override any parameter set
#in config file.

use DBI;
use DBI::DBD; #Just to test everything is okay
use Curses;
use Data::Dumper;

sub Print_Usage()
{
  print "$0 : display top network activity per user\n";
  print "GENERAL OPTIONS\n";
  print "\t-c\t\t\tconfig file name (default is /etc/nufw/nutop.conf)\n";
  print "\t-delay\t\t\tDelay for refresh (in seconds) (default : 2)\n";
  print "\t-line\t\t\tNumbers of user lines to output (default : 20)\n";
  print "\t-help\t\t\tDisplay this help message and exit\n";
  print "DATABASE RELATED OPTIONS\n";
  print "\t-version\t\tNuFW protocol version. 'Normal' is 2, use 1 for old version\n";
  print "\t-databasetype\t\tdatabase choice (pgsql or mysql) [default is mysql]\n";
  print "\t-host\t\t\tdatabase adress (IP or name) default is 127.0.0.1)\n";
  print "\t-user\t\t\tdatabase username (default is \'nutop\')\n";
  print "\t-pass\t\t\tdatabase password (no default)\n";
  print "\t-databasename\t\tdatabase to use (default is \'nulog\')\n";
  print "\t-tablename\t\ttable to use (default is \'ulog\')\n";
  print "\t-port\t\t\tTCP port to use (default is 3306/5432 for mysql/pgsql respectively)\n";
}

my $modified_conf=0;
my %config = ('databasetype' => 'mysql',
              'host' => '127.0.0.1',
              'user' => 'nutop',
              'databasename' => 'nulog',
              'tablename' => 'ulog',
              'delay' => 2,
              'line' => 20,
              'version' => 2,
              ); #This hash contains all configuration variables

sub die_politely($)
{
  my $mess = shift @_;
  print STDERR $mess."\n";
  print STDERR "Maybe you need to configure this program in ".$config{"configfile"}.  "?\n";
  print STDERR "or run me with \"--help\"\n";
  exit(-1);
}

sub long2ip{
  my (@octets,$i,$ip_number,$ip_number_display,$number_convert,$ip_address);
  $ip_number_display = $ip_number = shift;
  chomp($ip_number);
		for($i = 3; $i >= 0; $i--) {
		  $octets[$i] = ($ip_number & 0xFF);
		  $ip_number >>= 8;
		}
return join('.', @octets);
}


my %tmpconfig; #yucks, getopt can only be called once
GetOptions( "c=s" => \$config{'configfile'},
            "databasetype=s" => \$tmpconfig{'databasetype'},
            "host=s" => \$tmpconfig{'host'},
            "user=s" => \$tmpconfig{'user'},
            "pass=s" => \$tmpconfig{'pass'},
            "databasename=s" => \$tmpconfig{'databasename'},
            "tablename=s" => \$tmpconfig{'tablename'},
            "delay=i" => \$tmpconfig{'delay'},
            "line=i" => \$tmpconfig{'line'},
            "version=i" => \$tmpconfig{'version'},
            "help" => \$tmpconfig{'help'},
);

if (defined($tmpconfig{'help'}))
{
  Print_Usage;
  exit 0;
}

defined $config{'configfile'} or $config{'configfile'} = '/etc/nufw/nutop.conf';
if (stat($config{'configfile'}))
{
  open (FILE,$config{'configfile'}) or print STDERR "Could not open file : ".$config{'configfile'}."\n";


  sub Parse_Config_File($)
  {
    my $config_hash = shift @_;
    my $linecnt = 0;
    while (my $line = <FILE>)
    {
      $linecnt++;
      $line =~ /^#/ and next;
      $line =~ /^$/ and next;
      unless ($line =~ /^(\S+)\s*=\s*(.*)$/)
      {
        print STDERR "Warning : config file line $linecnt : strange data got ignored\n";
        next;
      }
      $$config_hash{$1} = $2;
      $modified_conf++;
    }
    return %$config_hash;
  }
  %config = Parse_Config_File(\%config);
}
foreach my $key(keys %tmpconfig)
{
  if (defined $tmpconfig{$key}){
    $config{$key} = $tmpconfig{$key};
    $modified_conf++;
  }
}


if (!$modified_conf)
{
  die_politely ("No given parameter, nothing in config file");
}

#Lets check input now
{
  my $error = 0;
  unless ($config{'databasetype'} =~ /^(?:mysql|pgsql)$/)
  {
    $error ++;
    print STDERR "E: databasetype seems to be set to something I don't understand.\n";
  }
  if ($config{'databasetype'} =~ /mysql/)
  {
    require DBD::mysql;
    defined ($config{'port'}) or $config{'port'} = 3306;
  }elsif ($config{'databasetype'} =~ /pgsql/)
  {
    require DBD::Pg;
    defined ($config{'port'}) or $config{'port'} = 5432;
  }
  if (($config{'version'}!=1) and ($config{'version'}!=2))
  {
    $error++;
    print STDERR "E: version must be either 1 or 2\n";
  }
#All for now, maybe I (or you?)'ll find some more to check
  if ($error > 0)
  {
    Print_Usage;
    exit 1;
  }
}



#Now we prepare connection to our database
my $dbh;
{
  my $datasource;
  $config{'databasetype'} =~ /mysql/ and
    $datasource = "dbi:mysql:database=".$config{'databasename'}.";host=".$config{'host'}.";port=".$config{'port'};
  $config{'databasetype'} =~ /pgsql/ and
    $datasource = "dbi:Pg:dbname=".$config{'databasename'}.";host=".$config{'host'}.";port=".$config{'port'};
  $dbh = DBI->connect($datasource, $config{'user'}, $config{'pass'}) or die_politely($DBI::errstr);
}

my @state=('DROP','OPEN','ESTAB','CLOSE');
my @delays=(1,5,15,60);
my $delayswitch=-1;

my @connection_query=();
push @connection_query,'username' if $config{'version'} == 2;
$config{'databasetype'} =~ /mysql/ and
push @connection_query,('user_id','ip_saddr','ip_daddr','tcp_sport','tcp_dport','DATE_FORMAT(start_timestamp,\'%D %M %Y %H:%i:%S\')','state');
$config{'databasetype'} =~ /pgsql/ and
push @connection_query,('user_id','ip_saddr','ip_daddr','tcp_sport','tcp_dport','start_timestamp','state');
my %connection_query_info=(number=>["Number",12],username=>["Name",12],user_id=>["ID",6],ip_saddr=>["Source IP",17],ip_daddr=>["Destination IP",17],ip_protocol=>["Proto",7],oob_time_sec=>["Open Time",11], tcp_sport=>["Sport",8], tcp_dport=>["Dport",8],id=>["Id Packet",12],'DATE_FORMAT(start_timestamp,\'%D %M %Y %H:%i:%S\')'=>["Start Time", 30],start_timestamp=>["Start Time",13],state=>["State",6]);

my @users_query=();
push @users_query,'username' if $config{'version'} == 2;
push @users_query,('user_id','state');


my $gal_query="select ".join (',' ,@connection_query) ." from ".$config{'tablename'};
$config{'databasetype'} =~ /pgsql/ and
  $gal_query="select ".join (',' ,@connection_query) ." from ". $config{'tablename'};

my $time_cond_query = " order by oob_time_sec DESC,oob_time_usec DESC limit 0,".$config{'line'};
$config{'databasetype'} =~ /pgsql/ and
  $time_cond_query=" order by oob_time_sec DESC,oob_time_usec DESC LIMIT ".$config{'line'};

my $users_cond_query_start="SELECT ".join (',',@users_query).",COUNT(*) AS number from ".$config{'tablename'};
my $users_cond_query_end=" GROUP BY user_id,state ORDER BY number DESC LIMIT 0,".$config{'line'};

$config{'databasetype'} =~ /pgsql/ and
  $users_cond_query_end=" LIMIT ".$config{'line'};

my $tcp_cond_query=" ip_protocol=6";
my $udp_cond_query=" ip_protocol=17";

my $time_users_cond_start = " start_timestamp > date_add(now(),interval -";
my $time_users_cond_end = " minute)";

if ($config{'databasetype'} =~ /pgsql/)
{
  $time_users_cond_start = " start_timestamp";
}

initscr();

noecho();
#cbreak();
halfdelay(10*$config{'delay'});

#start_color;
#init_pair(2,COLOR_YELLOW,COLOR_BLACK);
#init_pair(3,COLOR_GREEN,COLOR_BLACK);
#init_pair(4,COLOR_BLUE,COLOR_BLACK);
#init_pair(1,COLOR_RED,COLOR_BLACK);

my $mode = 'time';
my $proto = 'tcp';

while (1)
{
  my $count = 1; my $hpos=0;
  addstr(0,0,'nulog');
  my $time = localtime;
  my @times = ($time->hour,$time->min,$time->sec);
  foreach (@times){
    length == 1 and s/^/0/;
  }
  clear();
  addstr(0,7,$times[0].":".$times[1].":".$times[2]);
  addstr(0,19,"Sorted by ".$mode);
  $mode eq 'users' and addstr(0,37,"(last ".$delays[$delayswitch]." min)");
  addstr(0,54,"protocol :".$proto);
  eval {attron(A_REVERSE) };
#  eval {attron(A_BOLD) };
  addstr($count,0," "x90);
  my $query = $gal_query;
  $mode eq 'users' and $query=$users_cond_query_start.' WHERE'.$time_users_cond_start.$delays[$delayswitch].$time_users_cond_end;
  if ($proto eq 'tcp')
  {
    if ($mode eq 'time')
    {
      $query=$query.' WHERE'.$tcp_cond_query;
    }else{
      $query=$query.' AND'.$tcp_cond_query;
    }
  }
  if ($proto eq 'udp')
  {
    if ($mode eq 'time')
    {
      $query=$query.' WHERE'.$udp_cond_query;
    }else{
      $query=$query.' AND'.$udp_cond_query;
    }
  }
  $mode eq 'time' and $query=$query.$time_cond_query;
  $mode eq 'users' and $query=$query.$users_cond_query_end;
  my $sth = $dbh->prepare($query);
  my @display = @connection_query;
  if ($mode eq 'users')
  {
    @display = @users_query;
    push @display,'number';
  }
  if ( $sth->execute) {
    foreach my $field ( @display ){
       addstr($count, $hpos, @{$connection_query_info{$field}}[0]);
       $hpos+=@{$connection_query_info{$field}}[1];
    }
    eval {attrset(A_NORMAL) };
    while ( my @row = $sth->fetchrow_array ) {
      my $col=0;
      $hpos=0;
      $count ++;
#      set_color_fg(COLOR_YELLOW);
#      color_set($row[6]+1,undef);
      foreach my $attr (@row){
        if (($config{'databasetype'} =~ /mysql/) and ($display[$col] =~/^(?:ip_saddr|ip_daddr)$/)) {
          addstr($count, $hpos, long2ip($attr));
#	  print long2ip($attr)."\t";
        } else {
          #if (not defined $attr){print "DEBUG : PROBLEME AVEC l'attribut $col\n";}
          if (($display[$col] eq 'state') and (defined $state[$attr]))
          {
            defined $attr and addstr($count, $hpos, $state[$attr]);
          }else{
            defined $attr and addstr($count, $hpos, $attr);
          }
#	  print $attr."\t";
        }
        $hpos += @{$connection_query_info{$display[$col]}}[1];
#        color_set(0);
      $col++;
    }
#    print "\n";
    }
  }
  refresh;
  my $in = getch();
  if ($in eq 'q')
  {
    endwin();
    exit 0;
  }
  $in eq 't' and $mode='time';
  if ($in eq 'u')
  {
    $delayswitch++;
    $mode='users';
    $delayswitch > $#delays and $delayswitch = 0;
  }
  $in eq 'T' and $proto='tcp';
  $in eq 'U' and $proto='udp';
  $in eq 'A' and $proto='all';
  if ($in eq 'f')
  {
    $in = '';
    addstr(0,64,"FROZEN VIEW");
    my $loop = 1;
    while ($loop)
    {
      $in = getch();
      $in eq 'f' and $loop=0;
      if ($in eq 'q')
      {
        endwin();
        exit 0;
      }
    }
  }
}

