#!/usr/bin/perl 
##########################################################################
#                                                                        #
# Perl Script for Resend lost Accouting packet to portaone Radius Server #
#                                                                        #
# Modified on 110324 by yannick.vaucher@switzernet.com                   #
#                                                                        #
# Switzernet(c)2011                                                      #
#                                                                        #
##########################################################################
# my modules
use strict;
use warnings;
use Sys::Syslog;
use POSIX;
use Config::IniFiles;
use Authen::Radius;
use Switch;
use DBI;
use Date::Manip;
use Time::Local;
##########################################################################
# Setting
my $lock_file = '/var/run/ast-resend-lost.pid';
my $runas_user = 'root';
my $config_dir = '/etc/astrad/config';
##########################################################################
# Globals
my ($dbh_Rad,$Rad_connected);
my ($DB_NAME_Rad,$DB_HOST_Rad,$DB_USER_Rad,$DB_PASS_Rad);
my ($RADIUS_Server,$RADIUS_Secret);
my @select_rad;
my $isel = 0;
my $consecutive_failed_tries= 0;
my $request;
my $base_interval=2;
#Enable debug
use constant DEBUG => 0;
##########################################################################
#                             Initialization                             #
##########################################################################
# Check if already running
if( -e $lock_file ) {
	open(PID,$lock_file);
	my $pid=<PID>;
	close PID;
	chomp $pid;
	if( !-e "/proc/$pid" ) {
		print STDERR "Lock file present, but no process with pid=$pid.\n";
		die "Can't delete lock file $lock_file\n" if !unlink $lock_file;
		print STDERR "Lock file has been removed.\n";
	} else {
		die "Lockfile present, another copy is punning pid=$pid\n";
	}
}
my ($name, $passwd, $uid, $gid) = getpwnam($runas_user) or die "$runas_user not in passwd file";;
##########################################################################
# set C locale
POSIX::setlocale(LC_ALL, "C");
##########################################################################
# Become daemon
my $pid;
if( !defined($pid = fork()) ) {
    die "cannot fork: $!";
} elsif ($pid) {
    # Create lockfile, and finish parent process
    open(PID, "> $lock_file") || die "ast-resend-lost.pl: Unable to create lockfile $lock_file\n";
    print PID "$pid";
    close PID;
    chown $uid, $gid, $lock_file;
    exit;
} else {
    # daemon
    setpgrp();
    select(STDERR); $| = 1;
    select(STDOUT); $| = 1;
    openlog('ast-resend-lost', 'cons,pid', 'daemon');
    syslog('notice', "Ast-Res: Resend lost Accounting packets for Asterisk started");
}

##########################################################################
# Install signall handler
$SIG{INT} = \&safe_exit;
$SIG{QUIT} = \&safe_exit;
$SIG{TERM} = \&safe_exit;
$SIG{HUP} = \&load_config;
##########################################################################
# Drop privileges
setuid($uid);
$< = $uid;
$> = $uid;
##########################################################################
# Signal Handlers
sub safe_exit {
    my($sig) = @_;
    syslog('crit', "Ast-Res: Caught a SIG$sig - shutting down");
    $dbh_Rad->disconnect if $Rad_connected;
    unlink $lock_file or syslog('crit', "Ast-Res: Unable to create lockfile $lock_file\n");
    closelog();     
    exit;
}


##########################################################################
#                                FUNCTION                                #
##########################################################################
# sample '09:16:05 GMT Sat Dec 11 2004'
sub format_date_sql_2_gmt {
    my ($sql_date) = @_;
    my $date = &ParseDate($sql_date);
    my $gmt_date = &Date_ConvTZ($date,"","GMT");

    return &UnixDate($gmt_date, "%H:%M:%S GMT %a %b %e %Y");
}


##########################################################################
# Load config from mysql.conf
sub load_config {
    my $conf=Config::IniFiles->new(-file => "$config_dir/switzer.conf");
    return 0 if !defined $conf ;
    $RADIUS_Server= $conf->val('MASTER_RD','RADIUS_SERVER');
    $RADIUS_Secret= $conf->val('MASTER_RD','RADIUS_SECRET');
    $DB_NAME_Rad  = $conf->val('ASTER_DB','DB_NAME');
    $DB_HOST_Rad  = $conf->val('ASTER_DB','DB_HOST');
    $DB_USER_Rad  = $conf->val('ASTER_DB','DB_USER');
    $DB_PASS_Rad  = $conf->val('ASTER_DB','DB_PASS');      
    return 1;
}


##########################################################################
# sample '2004-12-11 09:16:05'
sub format_date_sql {
    my ($date) = @_;

    if ($date eq 0) {
        return "NULL";
    } else {
        return strftime("%Y-%m-%d %H:%M:%S", localtime($date));
    }
}
##########################################################################
# Astrad Radius DB connect 
sub Rad_connect {
    my $cop=0;
    $Rad_connected = 1;
    while( 1 ) {
        $cop++;
        $dbh_Rad = DBI->connect_cached("DBI:mysql:$DB_NAME_Rad;host=$DB_HOST_Rad", 
                                       $DB_USER_Rad, $DB_PASS_Rad);
        if( $dbh_Rad ) {
            $Rad_connected = 1;
            $cop=0;
            syslog('info', 'Ast-Res: Connected to Astrad Radius DB!') if (DEBUG);
            return 1;
        } else {
            if ($Rad_connected) {
                syslog('err', 'Ast-Res: Could not connect to Astrad Radius DB!');
                $cop=1;
            }
            $Rad_connected = 0;
            syslog('info', "Ast-Res: Connection to Astrad Radius DB -> SLEEP $cop");
            $cop++ if $cop;
            return 0 if ($cop>=3);
        }
        sleep 1;
    }
}
##########################################################################
# Load register request from Astrad Reg. DB
sub load_lost_rad {
    return -1 if (!Rad_connect());
    my $cop=0;
    $isel = 0;
    undef @select_rad;
    my $request="SELECT h323_connect_time, h323_disconnect_time, acct_session_time, 
                     h323_disconnect_cause, nas_ip_address, user_name, 
                     calling_station_id, called_station_id, call_id, 
                     h323_setup_time, h323_conf_id, h323_remote_address,id,send_counter
                 FROM radius_packets_Failed ";
    $request .= "WHERE last_send < NOW() ";
    $request .= "ORDER BY id LIMIT 100;";
    my $sth = $dbh_Rad->prepare($request);
    return -1 if (!$sth->execute());
    while (my $result = $sth->fetchrow_hashref) {
        ${$select_rad[$isel]}[0]  = $result->{h323_connect_time}; 
        ${$select_rad[$isel]}[1]  = $result->{h323_disconnect_time}; 
        ${$select_rad[$isel]}[2]  = $result->{acct_session_time}; 
        ${$select_rad[$isel]}[3]  = $result->{h323_disconnect_cause}; 
        ${$select_rad[$isel]}[4]  = $result->{nas_ip_address}; 
        ${$select_rad[$isel]}[5]  = $result->{user_name}; 
        ${$select_rad[$isel]}[6]  = $result->{calling_station_id}; 
        ${$select_rad[$isel]}[7]  = $result->{called_station_id}; 
        ${$select_rad[$isel]}[8]  = $result->{call_id}; 
        ${$select_rad[$isel]}[9]  = $result->{h323_setup_time}; 
        ${$select_rad[$isel]}[10] = $result->{h323_conf_id}; 
        ${$select_rad[$isel]}[11] = $result->{h323_remote_address}; 
        ${$select_rad[$isel]}[12] = $result->{id}; 
        ${$select_rad[$isel]}[13] = $result->{send_counter}; 
        $isel++;
    }
    if (DEBUG) {
        syslog('info', "Ast-Res: request = $request");
        syslog('info', "Ast-Res: sth->rows = ".$sth->rows);
        syslog('info', "Ast-Res: isel = $isel");
    }
    $sth->finish;
    return 0 if ($isel == 0);
    return 1; 
}
##########################################################################
# Send lost radius packet to portaone server
sub send_lost_rad {
  return 0 if ($isel == 0);
  my ($cop,$cc,$successsend)=(0,0,0);
  if ($isel > 0) {
    my $r = new Authen::Radius(Host => $RADIUS_Server, Secret => $RADIUS_Secret, TimeOut => 10, Service => 'radius-acct');
    if( !defined $r ) {
        syslog('crit', "Ast-Res: RADIUS host $RADIUS_Server ERROR for Resending");
        return -1;
    }
    $request = "DELETE FROM radius_packets_Failed WHERE id IN (";
    while ($cop < $isel && $cc == 0) {
        $r->clear_attributes();

        my $disco_str_time = ${$select_rad[$cop]}[1];
        my ($year, $month, $day, $hour, $min, $sec) = ($disco_str_time =~ /(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/);
        my $disco_time = timelocal( $sec, $min, $hour, $day, ($month-1), $year);

	my $delay_time = time() - $disco_time;

        syslog('info', "Debug-Ast: " . $delay_time . " = " . localtime(time()) ." - ". localtime($disco_time). "(" . $disco_str_time . ")") if (DEBUG); 
        $r->add_attributes  (
            { Name => 'Acct-Status-Type', Value => 'Stop' },
            { Name => 'h323-call-origin', Value => 'originate' },
            { Name => 'h323-connect-time', Value => format_date_sql_2_gmt(${$select_rad[$cop]}[0]) },
            { Name => 'h323-disconnect-time', Value => format_date_sql_2_gmt(${$select_rad[$cop]}[1]) },
            { Name => 'Acct-Session-Time', Value => ${$select_rad[$cop]}[2] },
            { Name => 'h323-disconnect-cause', Value => ${$select_rad[$cop]}[3] },
            { Name => 'h323-call-type', Value => 'VoIP' },
            { Name => 'Cisco-AVPair', Value => 'session-protocol=sipv2' },
            { Name => 'NAS-IP-Address', Value => ${$select_rad[$cop]}[4] },
            { Name => 'User-Name', Value => ${$select_rad[$cop]}[5] },
            { Name => 'Calling-Station-Id', Value => ${$select_rad[$cop]}[6] },
            { Name => 'Called-Station-Id', Value => ${$select_rad[$cop]}[7] },
            { Name => 'Cisco-AVPair', Value => 'call-id='.${$select_rad[$cop]}[8] },
            { Name => 'h323-setup-time', Value => format_date_sql_2_gmt(${$select_rad[$cop]}[9]) },
            { Name => 'Cisco-AVPair', Value => 'h323-conf-id='.${$select_rad[$cop]}[10] },
            { Name => 'h323-remote-address', Value => ${$select_rad[$cop]}[11] },
            { Name => 'Acct-Delay-Time', Value =>  $delay_time}
        );
        $r->send_packet (ACCOUNTING_REQUEST) and my $type = $r->recv_packet; 
        if (defined $type) {
            $request = $request . "'".${$select_rad[$cop]}[12]."'," ;
            $consecutive_failed_tries = 0;
            $successsend++;
        } else {
            ${$select_rad[$cop]}[13]++;
            ${$select_rad[$cop]}[14] = format_date_sql(ceil($base_interval**${$select_rad[$cop]}[13])+time);
            $consecutive_failed_tries++;
            $cc=-2;
            my $errCode=Authen::Radius::get_error();
            my $errCodeMsg = 'Unknown';
            $errCodeMsg=$errCode." (".Authen::Radius::strerror($errCode).")" if($errCode);
            syslog('crit', "Ast-Res: Error ".$errCodeMsg." sending Radius packet. Attempt number ".${$select_rad[$cop]}[13].". Next attempt at ".${$select_rad[$cop]}[14]) if DEBUG;
            my $request2 = "UPDATE radius_packets_Failed SET last_send = '".${$select_rad[$cop]}[14]."', send_counter = ".${$select_rad[$cop]}[13]." WHERE id=".${$select_rad[$cop]}[12].";";
            print $request2."\n" if DEBUG;
            my $sth = $dbh_Rad->prepare($request2);
            $sth->execute();
            $sth->finish;
            undef $sth;
        }
        $cop++;
    }
    $isel = $cop;
    $request = substr($request,0,-1) . " );";
    syslog('info', "Ast-Res: request = $request") if (DEBUG);
  } else {
    $successsend++;
    $isel = 0 - $isel;
  }
  if ($successsend > 0) {
    print $request."\n" if DEBUG;
    my $sth = $dbh_Rad->prepare($request);
    if ($sth->execute()) {
       $sth->finish;
       return $cc;
    }
    syslog('crit', "Ast-Res: Error deleting from table ".$sth->errstr);
    syslog('info', "Ast-Res: request = ".$request);
    return -3;
  }
  return -1;
}
##########################################################################
#                                 main()                                 #
##########################################################################
#TODO: Add fields to mysql of last attempt and do a select using those fields
#TODO: Verify if the h323 id already exists before adding it for calls marked as timeout
#TODO: Verify in which conditions we have repeated calls in table

Authen::Radius->load_dictionary;
load_config();
my $sleep_time = 0;
LINE: while ( 1 ) {
    if ($isel == 0) {
      switch (load_lost_rad()) {
        case  1	{ syslog('info', "Ast-Res: Successfully selection from table -> $isel items")}
	case  0 { syslog('info', "Ast-Res: Empty selection from table"); sleep 5; next LINE}
	case -1 { syslog('crit', "Ast-Res: Error in Radius MySQL connection"); sleep 1; next LINE}
      }
    }
    switch (send_lost_rad()) {             
	case  0	{ syslog('info', "Ast-Res: Successfully sending lost packets -> $isel items"); $isel = 0; undef @select_rad; sleep 5}
        case -1 { syslog('info', "Ast-Res: Error in Sending lost packets. 0 of $isel items successfuly sent. Sleeping ".$sleep_time); sleep 1; $isel=0; undef @select_rad}
	case -2 { syslog('info', "Ast-Res: Error in Sending lost packets. $isel items successfuly sent. Sleeping ".$sleep_time); sleep $sleep_time; $isel=0; undef @select_rad}
	case -3 { syslog('crit', "Ast-Res: Error executing query"); $isel = 0 - $isel; sleep 30}
    }
}
##########################################################################
#                                  END                                   #
##########################################################################


