Skip to content
Snippets Groups Projects
Forked from smradius / smradius
707 commits behind the upstream repository.
acct-filter 26.48 KiB
#!/usr/bin/perl -w
# Author: Nigel Kukard  <nkukard@lbsd.net>
# Date: 17/04/2007
# Desc: Accounting filter for GNU Radius
# License: GPL

use strict;
use Benchmark;
use Getopt::Long;
use DateTime;
use Time::HiRes qw( gettimeofday tv_interval );

use MIME::Lite;

# Set library directory
use lib qw(../../);

use sm::config;
use sm::dblayer;

# Radius stuff
use Authen::Radius;

# Common stuff
require("common.pm");

use Data::Dumper;


# Notify constants
use constant {
	NOTIFY_CHECK	=> 1,
	NOTIFY_RESET	=> 2,
};


my %optctl = ();

GetOptions(\%optctl, "help");


# Check if user wants usage
if (defined($optctl{'help'})) {
	displayUsage();
}

# Open up logfile
my $logfile = "/var/log/radius/acct-filter";
open(FH,">> $logfile") or die "Failed to open '$logfile': $!";


# Load radius dictionaries
Authen::Radius->load_dictionary("raddb/dictionary");


# Databases
my $dbh;  # Authentication
my $dbh_log;  # Logs

# Get db handle
$dbh = sm::dbilayer->new($cfg_db_DSN, $cfg_db_Username, $cfg_db_Password);
if (!$dbh) {
	print(STDERR "Error creating database object: ".sm::dbilayer->internalErr());
	exit 1;
}

# Connect to database
if ($dbh->connect() != 0) {
	print(STDERR "Error connecting to database: ".$dbh->err); 
	exit 1;
}

# Check if we must use split db's
if (defined($cfg_radiuslog_db_DSN)) {
	# Get log db handle
	$dbh_log = sm::dbilayer->new($cfg_radiuslog_db_DSN, $cfg_radiuslog_db_Username, $cfg_radiuslog_db_Password);
	if (!$dbh_log) {
		print(STDERR "Error creating database object: ".sm::dbilayer->internalErr());
		exit 1;
	}

	# Connect to database
	if ($dbh_log->connect() != 0) {
		print(STDERR "Error connecting to database: ".$dbh_log->err); 
		exit 1;
	}
# If not use the main DB
} else {
	$dbh_log = $dbh;
}



# Signal handler for dead children
use POSIX "WNOHANG";
sub REAPER {
	my $child;
	# If a second child dies while in the signal handler caused by the
	# first death, we won't get another signal. So must loop here else
	# we will leave the unreaped child as a zombie. And the next time
	# two children die we get another zombie. And so on.
	while (($child = waitpid(-1,WNOHANG)) > 0) {
#		$Kid_Status{$child} = $?;
		1;
	}
	$SIG{CHLD} = \&REAPER;  # still loathe sysV
}
$SIG{CHLD} = \&REAPER;



# No buffering
select((select(FH), $| = 1)[0]);
select((select(STDOUT), $| = 1)[0]);


# Handled requests
my $requests = 0;

# Loop with input
while (my $line = <STDIN>) {
	my %acct;
	my @acct;


	# Inc number of requests
	$requests++;

	# Munch off \n
	chomp($line);

	# Check number of results
	if ((@acct = split /:/, $line) != 21) {
		print(FH "ERROR: Number of params from radiusd: ".(@acct)."/$line\n");
		goto END;
	}

	# Pull in request
	($acct{'User-Name'},$acct{'Status-Type'},$acct{'Acct-Session-Id'},$acct{'NAS-IP-Address'},$acct{'NAS-Port-Type'},$acct{'NAS-Port'},
			$acct{'Called-Station-Id'},$acct{'Calling-Station-Id'},$acct{'Delay'},$acct{'Acct-Session-Time'},
			$acct{'Acct-Input-Octets'},$acct{'Acct-Output-Octets'},$acct{'Acct-Input-Gigawords'},$acct{'Acct-Output-Gigawords'},
			$acct{'Ascend-Xmit-Rate'},$acct{'Ascend-Data-Rate'},$acct{'Framed-IP-Address'},$acct{'Connect-Info'},$acct{'Service-Type'},
			$acct{'Class'},$acct{'Acct-Terminate-Cause'}) = @acct;

	# Pull timestamp	
	my $dt = DateTime->from_epoch( epoch => time() );
	$acct{'Timestamp'} = $dt->strftime('%Y-%m-%d %H:%M:%S');

	my $timer0 = [gettimeofday];

	# Grab user details
	my $userData = getUser($dbh,$acct{'User-Name'});
	if (ref $userData ne "HASH") {
		print(FH "ERROR: $userData\n");
		$userData->{'AgentID'} = 0;
		$userData->{'RadiusClassID'} = 0;
		$userData->{'UsageCap'} = 0;
	}

	printf(FH 'ACCT - AgentID: %s, ClassID: %s, User-Name: %s, Status-Type: %s, Timestamp: %s, Acct-Session-Id: %s, NAS-IP-Address: %s, NAS-Port-Type: %s, NAS-Port: %s, Called-Station-Id: %s, Calling-Station-Id: %s, Delay: %s, Acct-Session-Time: %s, Acct-Input-Octets: %s, Acct-Output-Octets: %s, Acct-Input-Gigawords: %s, Acct-Output-Gigawords: %s, Ascend-Xmit-Rate: %s, Ascend-Data-Rate: %s, Framed-IP-Address: %s, Connect-Info: %s, Service-Type: %s, Class: %s, Acct-Terminate-Cause: %s'."\n",$userData->{'AgentID'},$userData->{'RadiusClassID'},$acct{'User-Name'},$acct{'Status-Type'},$acct{'Timestamp'},$acct{'Acct-Session-Id'},$acct{'NAS-IP-Address'},$acct{'NAS-Port-Type'},$acct{'NAS-Port'},$acct{'Called-Station-Id'},$acct{'Calling-Station-Id'},$acct{'Delay'},$acct{'Acct-Session-Time'},$acct{'Acct-Input-Octets'},$acct{'Acct-Output-Octets'},$acct{'Acct-Input-Gigawords'},$acct{'Acct-Output-Gigawords'},$acct{'Ascend-Xmit-Rate'},$acct{'Ascend-Data-Rate'},$acct{'Framed-IP-Address'},$acct{'Connect-Info'},$acct{'Service-Type'},$acct{'Class'},$acct{'Acct-Terminate-Cause'});
	
	#	IF ADSL  (ADSL specific stuff)
	if ($acct{'NAS-Port-Type'} eq "5") {
		# Calculate dates
		my $date = DateTime->from_epoch( epoch => time() );
		my $today = $date->ymd();
		$date->set_day(1);
	    my $thismonth = $date->ymd();
		$date->add( months => 1);
	    my $nextmonth = $date->ymd();
		
		my $extraQuery = "";


		# NULL - uncapped, no limits
		if (!defined($userData->{'UsageCap'})) {
			$extraQuery = "AND Timestamp > ".$dbh->quote($thismonth)." AND Timestamp < ".$dbh->quote($nextmonth);

		# > 0 - normal cap, check usage for this month, check topups for this month
		# Calculate cap user has  (acctinputoctets + (2^32 * gigawords)) /1024 / 1024
		# Calculate usage user has
		} elsif ($userData->{'UsageCap'} > 0) {
			$extraQuery = "AND Timestamp > ".$dbh->quote($thismonth)." AND Timestamp < ".$dbh->quote($nextmonth);

		# 0 - topup account
		} elsif ($userData->{'UsageCap'} == 0) {

		}

		# Grab users usage
		my $usageData = getUsage($dbh_log,$acct{'User-Name'},$extraQuery);
		if (ref $usageData ne "HASH") {
			print(FH "ERROR: $usageData\n");
#			print(STDOUT "1\n");
			goto END;
		}
		$userData->{'TotalUsage'} = $usageData->{'Total'};
		
		# Only total up this month
		$extraQuery = "";
		if (defined($userData->{'UsageCap'}) && $userData->{'UsageCap'} > 0) {
			$extraQuery = "AND ValidFrom <= ".$dbh->quote($today)." AND ValidTo > ".$dbh->quote($today);
		}
		# Get how much we've been topped up
		my $topupData = getTopups($dbh,$acct{'User-Name'},$extraQuery);
		if (ref $topupData ne "HASH") {
			print(FH "ERROR: $topupData\n");
#			print(STDOUT "1\n");
			goto END;
		}
		$userData->{'Topups'} = $topupData->{'Total'};

		$userData->{'SessUsage'} = 0;
		# If we updating or disconnecting, calculate the session usage so far
		if ($acct{'Status-Type'} == 2 || $acct{'Status-Type'} == 3) {
			# Check how much data we used in this session
			my $sessUsage = 0; 
			if (defined($acct{'Acct-Input-Octets'}) && $acct{'Acct-Input-Octets'} > 0) {
				$sessUsage += $acct{'Acct-Input-Octets'} / 1024 / 1024;
			}
			if (defined($acct{'Acct-Input-Gigawords'}) && $acct{'Acct-Input-Gigawords'} > 0) {
				$sessUsage += $acct{'Acct-Input-Gigawords'} * 4096;
			}
			# Add up output
			if (defined($acct{'Acct-Output-Octets'}) && $acct{'Acct-Output-Octets'} > 0) {
				$sessUsage += $acct{'Acct-Output-Octets'} / 1024 / 1024;
			}
			if (defined($acct{'Acct-Output-Gigawords'}) && $acct{'Acct-Output-Gigawords'} > 0) {
				$sessUsage += $acct{'Acct-Output-Gigawords'} * 4096;
			}
			$userData->{'SessUsage'} = ceil($sessUsage);
		}

		# Print usage stats
		printf(FH '     - Usage => Total: %s, Session: %s, Cap: %s, Topups: %s'."\n",
					$userData->{'TotalUsage'},
					$userData->{'SessUsage'},
					defined($userData->{'UsageCap'}) ? $userData->{'UsageCap'} : "uncapped",
					$userData->{'Topups'},
		);

		# Capping & usage predictions
		my $totalCap = !defined($userData->{'UsageCap'}) ? 0 : $userData->{'UsageCap'} + $userData->{'Topups'};
		if (defined($userData->{'UsageCap'}) && $userData->{'UsageCap'} >= 0) {
			my $exceeded = 0;

			# Checking if we updating or stopping the session, if we are, check capping
			if ($totalCap <= $userData->{'TotalUsage'} && ($acct{'Status-Type'} == 2 || $acct{'Status-Type'} == 3)) {
				print(FH "     - TEST: User has exceeded cap by ".($userData->{'TotalUsage'} - $totalCap)."Mbyte\n");

				# If this is an update, user is still logged in, so disconnect them
				if ($userData->{'CappingType'} == 1 && $acct{'Status-Type'} == 3) {
					print(FH "     - TEST: User is still logged in, disconnecting\n");
					disconnectUser($dbh,\*FH,$userData,\%acct);

				# If user just got kicked off, notify them
				} elsif ($userData->{'CappingType'} == 1 && $acct{'Status-Type'} == 2) {
					print(FH "     - TEST: Notifying user\n");
					# We reset notifications, cause the user MUST get this and we dont mind updating him in future
					notifyUser($dbh,$dbh_log,\*FH,$userData,NOTIFY_RESET,
							sprintf('Username %s has been capped, please contact your ISP should you need a topup',$userData->{'Username'}),
					);
				}

				$exceeded = 1;
			}

			# Check if we may exceed the cap in the next hour, we need at least 1hrs of data for accuracy
			if (!$exceeded && $acct{'Acct-Session-Time'} > 3600) {
				my $perSecUsage = $userData->{'SessUsage'} / $acct{'Acct-Session-Time'};
				my $hrPredict = sprintf('%.2f',($perSecUsage * 3600));

				print(FH "     - User is predicted to use ${hrPredict}Mb in the next hour\n");
				# If user is infact predicted to exceed, notify them
				if ($totalCap <= $userData->{'TotalUsage'} + $hrPredict) {
					print(FH "     - This will exceed users cap, notifying user\n");
					notifyUser($dbh,$dbh_log,\*FH,$userData,NOTIFY_CHECK,
							sprintf('Username %s may be capped in the next hour based on current usage stats, please contact your ISP should you need a topup',
									$userData->{'Username'}),
					);
				}
			}
		}
	}

	# START
	if ($acct{'Status-Type'} == 1) {
		# Start accounting
		my $res = $dbh_log->do("
				INSERT INTO radiusLogs
					(
						AgentID,
						Username,
						RadiusClassID,
						UsageCap,
						Topups,
						Timestamp,
						AcctDelayTime,
						AcctSessionID,
						NASIPAddress,
						NASPortType,
						NASPort,
						CalledStationID,
						CallingStationID,
						ConnectInfo,
						ServiceType,
						Class,
						FramedIPAddress,
						Status
					)
				VALUES
					(
						".$dbh_log->quote($userData->{'AgentID'}).",
						".$dbh_log->quote($acct{'User-Name'}).",
						".$dbh_log->quote($userData->{'RadiusClassID'}).",
						".$dbh_log->quote($userData->{'UsageCap'}).",
						".$dbh_log->quote($userData->{'Topups'}).",
						".$dbh_log->quote($acct{'Timestamp'}).",
						".$dbh_log->quote($acct{'Delay'}).",
						".$dbh_log->quote($acct{'Acct-Session-Id'}).",
						".$dbh_log->quote($acct{'NAS-IP-Address'}).",
						".$dbh_log->quote($acct{'NAS-Port-Type'}).",
						".$dbh_log->quote($acct{'NAS-Port'}).",
						".$dbh_log->quote($acct{'Called-Station-Id'}).",
						".$dbh_log->quote($acct{'Calling-Station-Id'}).",
						".$dbh_log->quote($acct{'Connect-Info'}).",
						".$dbh_log->quote($acct{'Service-Type'}).",
						".$dbh_log->quote($acct{'Class'}).",
						".$dbh_log->quote($acct{'Framed-IP-Address'}).",
						".$dbh_log->quote(1)."
					)
		");
		if (!$res) {
			print(FH "ERROR: Failed to insert radius auth fail data: ".$dbh_log->err."\n");
#			print(STDOUT "1\n");
			goto END;
		}

	# STOP
	} elsif ($acct{'Status-Type'} == 2) {
		# Stop accounting
		my $res = $dbh_log->do("
				UPDATE radiusLogs
				SET
					Status = ".$dbh_log->quote(3).",
					NASTransmitRate = ".$dbh_log->quote($acct{'Ascend-Xmit-Rate'}).",
					NASReceiveRate = ".$dbh_log->quote($acct{'Ascend-Data-Rate'}).",
					AcctSessionTime = ".$dbh_log->quote($acct{'Acct-Session-Time'}).",
					AcctInputOctets = ".$dbh_log->quote($acct{'Acct-Input-Octets'}).",
					AcctOutputOctets = ".$dbh_log->quote($acct{'Acct-Output-Octets'}).",
					AcctInputGigawords = ".$dbh_log->quote($acct{'Acct-Input-Gigawords'}).",
					AcctOutputGigawords = ".$dbh_log->quote($acct{'Acct-Output-Gigawords'}).",
					ConnectTermReason = ".$dbh_log->quote($acct{'Acct-Terminate-Cause'}).",
					LastAccUpdate = ".$dbh_log->quote($acct{'Timestamp'})."
				WHERE
					Username = ".$dbh_log->quote($acct{'User-Name'})."
					AND AcctSessionID = ".$dbh_log->quote($acct{'Acct-Session-Id'})."
					AND NASIPAddress = ".$dbh_log->quote($acct{'NAS-IP-Address'})."
		");
		if (!$res) {
			print(FH "ERROR: Failed to update stop accounting data: ".$dbh_log->err."\n");
#			print(STDOUT "1\n");
			goto END;
		}
		
		$res = 0 if ($res eq "0E0");

		print(FH "     - Rows updated: $res\n");
		
		# Check if we updated duplicates, if we did, fix them
		if ($res > 1) {
			fixDuplicates(\%acct);
		}

	# UPDATE
	} elsif ($acct{'Status-Type'} == 3) {
		# Update accounting
		my $res = $dbh_log->do("
				UPDATE radiusLogs
				SET
					Status = ".$dbh_log->quote(2).",
					NASTransmitRate = ".$dbh_log->quote($acct{'Ascend-Xmit-Rate'}).",
					NASReceiveRate = ".$dbh_log->quote($acct{'Ascend-Data-Rate'}).",
					AcctSessionTime = ".$dbh_log->quote($acct{'Acct-Session-Time'}).",
					AcctInputOctets = ".$dbh_log->quote($acct{'Acct-Input-Octets'}).",
					AcctOutputOctets = ".$dbh_log->quote($acct{'Acct-Output-Octets'}).",
					AcctInputGigawords = ".$dbh_log->quote($acct{'Acct-Input-Gigawords'}).",
					AcctOutputGigawords = ".$dbh_log->quote($acct{'Acct-Output-Gigawords'}).",
					ConnectTermReason = ".$dbh_log->quote($acct{'Acct-Terminate-Cause'}).",
					LastAccUpdate = ".$dbh_log->quote($acct{'Timestamp'})."
				WHERE
					Username = ".$dbh_log->quote($acct{'User-Name'})."
					AND AcctSessionID = ".$dbh_log->quote($acct{'Acct-Session-Id'})."
					AND NASIPAddress = ".$dbh_log->quote($acct{'NAS-IP-Address'})."
		");
		if (!$res) {
			print(FH "ERROR: Failed to update accounting data: ".$dbh_log->err."\n");
#			print(STDOUT "1\n");
			goto END;
		}

		$res = 0 if ($res eq "0E0");

		print(FH "     - Rows updated: $res\n");

		# Create record as it doesn't exist!
		if ($res == 0) {
			# Start accounting
			my $res = $dbh_log->do("
					INSERT INTO radiusLogs
						(
							AgentID,
							Username,
							RadiusClassID,
							UsageCap,
							Topups,
							Timestamp,
							AcctDelayTime,
							AcctSessionID,
							NASIPAddress,
							NASPortType,
							NASPort,
							CalledStationID,
							CallingStationID,
							ConnectInfo,
							ServiceType,
							Class,
							FramedIPAddress,
							NASTransmitRate,
							NASReceiveRate,
							AcctSessionTime,
							AcctInputOctets,
							AcctOutputOctets,
							AcctInputGigawords,
							AcctOutputGigawords,
							ConnectTermReason,
							Status
						)
					VALUES
						(
							".$dbh_log->quote($userData->{'AgentID'}).",
							".$dbh_log->quote($acct{'User-Name'}).",
							".$dbh_log->quote($userData->{'RadiusClassID'}).",
							".$dbh_log->quote($userData->{'UsageCap'}).",
							".$dbh_log->quote($userData->{'Topups'}).",
							".$dbh_log->quote(
								$acct{'Timestamp'} - defined($acct{'Acct-Session-Time'}) ? $acct{'Acct-Session-Time'} : 0
							).",
							".$dbh_log->quote($acct{'Delay'}).",
							".$dbh_log->quote($acct{'Acct-Session-Id'}).",
							".$dbh_log->quote($acct{'NAS-IP-Address'}).",
							".$dbh_log->quote($acct{'NAS-Port-Type'}).",
							".$dbh_log->quote($acct{'NAS-Port'}).",
							".$dbh_log->quote($acct{'Called-Station-Id'}).",
							".$dbh_log->quote($acct{'Calling-Station-Id'}).",
							".$dbh_log->quote($acct{'Connect-Info'}).",
							".$dbh_log->quote($acct{'Service-Type'}).",
							".$dbh_log->quote($acct{'Class'}).",
							".$dbh_log->quote($acct{'Framed-IP-Address'}).",
							".$dbh_log->quote($acct{'Ascend-Xmit-Rate'}).",
							".$dbh_log->quote($acct{'Ascend-Data-Rate'}).",
							".$dbh_log->quote($acct{'Acct-Session-Time'}).",
							".$dbh_log->quote($acct{'Acct-Input-Octets'}).",
							".$dbh_log->quote($acct{'Acct-Output-Octets'}).",
							".$dbh_log->quote($acct{'Acct-Input-Gigawords'}).",
							".$dbh_log->quote($acct{'Acct-Output-Gigawords'}).",
							".$dbh_log->quote(0).",
							".$dbh_log->quote(1)."
						)
			");
			if (!$res) {
				print(FH "ERROR: Failed to insert radius auth fail data: ".$dbh_log->err."\n");
#				print(STDOUT "1\n");
				goto END;
			}
			print(FH "     - Lost accounting record created\n");
		}

		# Check if we updated duplicates, if we did, fix them
		if ($res > 1) {
			fixDuplicates(\%acct);
		}
	}
	
	my $timer1 = [gettimeofday];
	my $timediff = tv_interval($timer0,$timer1);
	print(FH "Code execution took: ${timediff}s\n");

END:
	# Check if we've handled enough requests
	if ($requests >= 1000) {
		print(FH "Handled enough request, terminating\n");
		last;
	}
#	print(STDOUT "0\n");
}
close(FH);


# Function to resolve duplicates
sub fixDuplicates
{
	my ($acct) = @_;


	# Select duplicates
	my $sth = $dbh_log->select("
			SELECT ID
			FROM
				radiusLogs
			WHERE
				Username = ".$dbh_log->quote($acct->{'User-Name'})."
				AND NASIPAddress = ".$dbh_log->quote($acct->{'NAS-IP-Address'})."
				AND AcctSessionID = ".$dbh_log->quote($acct->{'Acct-Session-Id'})."
			ORDER BY ID
			LIMIT 99 OFFSET 1
	");
	if (!$sth) {
		print(FH "ERROR: Selecting duplicates: ".$dbh_log->err."\n");
		print(STDOUT "1\n");
		return;
	}
	# Return if no rows returned
	return if ($sth->rows < 1);

	my @IDs = ();
	# Pull in duplicates
	while (my $dup = $sth->fetchrow_hashref()) {
		push(@IDs,$dup->{'ID'});
	}
	$sth->finish();
	# Remove duplicates
	my $res = $dbh_log->do("
			DELETE FROM radiusLogs
			WHERE
				ID IN (".join(',',@IDs).")
	");
	if (!$res) {
		print(FH "ERROR: Failed to remove duplicates: ".$dbh_log->err."\n");
	} else {
		$res = 0 if ($res eq "0E0");
		print(FH "     - Duplicates removed: $res\n");
	}
}


# Disconnect user
sub disconnectUser
{
	my ($dbh,$fh,$userData,$acct) = @_;


	# If radius classID == 0, means we don't know about this user, just return, nothing we can do
	if ($userData->{'RadiusClassID'} == 0) {
		print($fh "     - (D) Radius class ID is zero, cannot disconnect\n");
		return;
	}
	
	# Grab class data
	my $classData = getClass($dbh,$userData->{'RadiusClassID'});

	# Check if we got a hash, if not just return, error already reported in common.pm
	if (ref $classData ne "HASH") {
		print($fh "     - (D) No class data, cannot disconnect: $classData\n");
		return;
	}

	# Check if we have POD server list, if not ... return
	if (!defined($classData->{'PODServers'}) || $classData->{'PODServers'} eq "") {
		print($fh "     - (D) No POD servers, cannot disconnect\n");
		return;
	}

	# Loop with POD servers and add to list
	my @podServers;
	foreach my $i (split(/,/,$classData->{'PODServers'})) {
		my %server;
		# Pull out data we need
		if ($i =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/([^:]+):(\d+)$/) {
			($server{'IP'},$server{'Secret'},$server{'Port'}) = ($1,$2,$3);
		# If we didn't understand it, bitch
		} else {
			print($fh "     - (D) Did not understand POD server definition '$i'\n");
			return;
		}
		# Add to list
		push(@podServers,\%server);
	}

	# Fork off notification handler
	my $child_pid;
	if (!defined($child_pid = fork())) {
		print($fh "      - (D) ERROR: Cannot fork: $!\n");
	} elsif ($child_pid) {
		# I'm the parent
		return;
	} else {
		# I'm the child
		sleep(5);

		# Grab a server
		foreach my $server (@podServers)
		{
			# Fire up radius
			my $r = new Authen::Radius(Host => $server->{'IP'}.':'.$server->{'Port'}, Secret => $server->{'Secret'}, Debug => 1);
			if (!$r) {
				print($fh "FORK - (D) ".$userData->{'Username'}.", failed to create radius object\n");
				exit;
			}
			# Set attributes
			$r->add_attributes(
				{ 	Name => 'User-Name', 			Value => $userData->{'Username'} },
				{ 	Name => 'Framed-IP-Address', 	Value => $acct->{'Framed-IP-Address'} },
				{ 	Name => 'NAS-IP-Address', 		Value => $acct->{'NAS-IP-Address'} },
			);
			# Send packet
			my $res = $r->send_packet(DISCONNECT_REQUEST);
			print($fh "FORK - (D) ".$userData->{'Username'}.", Result1: ".Dumper($res)."\n");
			# Clear & grab result
			$r->clear_attributes();
			$res = $r->recv_packet();
			# Disect packet and see whats going on
			print($fh "FORK - (D) ".$userData->{'Username'}.", Result2: ".Dumper($res)."\n");
			my $value = $r->{'attributes'};
			if (defined($value)) {
			        my ($v1,$v2,$v3,$v4) = unpack('C C N',$value);
				printf($fh "FORK - (D) ".$userData->{'Username'}.", Got reply: v1=%s, v2=%s, v3=%s, v4=%s\n",
					defined($v1) ? $v1 : '<undef>',
					defined($v2) ? $v2 : '<undef>',
					defined($v3) ? $v3 : '<undef>',
					defined($v4) ? $v4 : '<undef>'
				);
				# Last it, as we got a reply
				last;
			# This would mean a timeout?
			} else {
				print($fh "FORK - (D) ".$userData->{'Username'}.", Got reply: undefined\n");
			}
		}
	} 
}



# Notify user
sub notifyUser
{
	my ($dbh,$dbh_log,$fh,$userData,$action,$message) = @_;


	# If no notification method is specified, just return
	return if (!defined($userData->{'NotifyMethod'}) || $userData->{'NotifyMethod'} eq "");

	# Get current notification status
	my $notifyStatus = getNotifyStatus($dbh_log,$userData);
	if (ref $notifyStatus ne "HASH") {
		print($fh "     - (N) No notify status: $notifyStatus\n");
		return;
	}
	printf($fh '     - (N) Notify status, id = %s, lastupdate = %s, updateinterval = %s'."\n",
			defined($notifyStatus->{'ID'}) ? $notifyStatus->{'ID'} : 'new',
			$notifyStatus->{'LastUpdate'},
			$notifyStatus->{'UpdateInterval'});

	# Time now
	my $now = time();

	# Check if we should reset
	if ($action & NOTIFY_RESET == NOTIFY_RESET) {
		$notifyStatus->{'UpdateInterval'} = 86400;
	
	# If we not resetting, check if we should do a check
	} else {
		# Calculate delta
		my $delta = (($now - $notifyStatus->{'LastUpdate'}) + $notifyStatus->{'UpdateInterval'}) / 2;
		$delta = 86400 if ($delta > 86400); # 1 day window only

		print($fh "     - (N) Notify delta = $delta\n");
	
		# Check actions
		if ($action & NOTIFY_CHECK == NOTIFY_CHECK) {
			# Return if delta is less than half a day
			return if ($delta < 43200);
			print($fh "     - (N) Notify check, succeeded\n");
		}

		$notifyStatus->{'UpdateInterval'} = $delta;
	}

	$notifyStatus->{'LastUpdate'} = $now;
	my $res = updateNotifyStatus($dbh_log,$userData,$notifyStatus);
	if (!$res) {
		print($fh "     - (N) Notify data updated\n");
	} else {
		print($fh "     - (N) Notify update error: $res\n");
	}

	# Get agent data
	my $agentData = getAgent($dbh,$userData->{'AgentID'});
	# Check if we got a hash, if not just return, error already reported in common.pm
	if (ref $agentData ne "HASH") {
		print($fh "     - (N) No agent data, cannot notify: $agentData\n");
		return;
	}

	# Fork off notification handler
	my $child_pid;
	if (!defined($child_pid = fork())) {
		print($fh "     - (N) ERROR: Cannot fork: $!\n");
	} elsif ($child_pid) {
		# I'm the parent
		return;
	} else {
		# I'm the child
		sleep(5);
	
		# Pull out notification email addy
		my @methods = split /,/, $userData->{'NotifyMethod'};

		print($fh "FORK - (N) Resume for: ".$userData->{'Username'}."\n");
		# Loop with notification methods
		foreach my $method (@methods) {

			# Check for email addy
			if ($method =~ /^\S+@\S+$/) {
				print($fh "FORK - (N) Its an email addy: $method\n");
		
				# Create message
				my $msg = MIME::Lite->new(
						From	=> $agentData->{'ContactEmail'},
						To		=> $method,
						Bcc		=> $agentData->{'ContactEmail'},
						Subject	=> "ADSL user ".$userData->{'Username'},
						Type	=> 'multipart/mixed'
				);

				# Attach body
				$msg->attach(
						Type	=> 'TEXT',
						Encoding 	=> 'quoted-printable',
						Data	=> $message,
				);

				# Send
				if (!(my $res = $msg->send())) {
					print($fh "FORK - (N) Failed to send email\n");
				}


			# First character can only be 1-9, next char is second part of country code, then 9 digit phone number (without 0)
			} elsif ($method =~ /^\+([1-9][0-9]{0,1})([1-9][0-9]{8})$/) {
				my $country = $1;
				my $number = $2;
					
				use URI::Escape;
				my $cmd = "echo '".uri_escape($message,"\x00-\x1f\x7f-\xff\"\'")."' | /usr/local/sitemanager-trunk/scripts/sms/sendsms --send-to='$country$number'";


				if ($country eq "27") {
					print($fh "FORK - (N) Its a cellphone in allowed country $country: $method\n");

					open(LOG,">> /var/log/radius/sms.log");
					printf(LOG '%s:SENT:%s:%s:%s%s', $userData->{'AgentID'}, $country . $number, $userData->{'Username'}, $cmd, "\n");
					close(LOG);

					print($fh "FORK - (N) Going to run command: $cmd\n");
					system($cmd);
					if ($? == -1) {
						print($fh "FORK - (N) Failed to execute: $!\n");
					} elsif ($? & 127) {
						printf($fh  "FORK - (N) Child died with signal %d\n",($? & 127));
					} else {
						printf($fh "FORK - (N) Child exited with value %d\n", $? >> 8);
					}

				} else {
					print($fh "FORK - (N) Its a cellphone in dis-allowed country $country: $method\n");

					open(LOG,">> /var/log/radius/sms.log");
					printf(LOG '%s:REJECT:%s:%s:%s%s', $userData->{'AgentID'}, $country . $number, $userData->{'Username'}, $cmd, "\n");
					close(LOG);
				}

			# Not understood
			} else {
				print($fh "FORK - (N) I DO NOT UNDERSTAND NOTIFY METHOD => '$method'\n");
			}
		}
		exit;
	} 
}


# Function to get a notification status
sub getNotifyStatus
{
	my ($dbh,$userData) = @_;


	my %notifyStatus;

	# Select tracking information
	my $sth = $dbh->select("
		SELECT 
			ID, Username, LastUpdate, UpdateInterval, LastValue
		FROM 
			radiusNotifyTrack
		WHERE 
			Username = ".$dbh->quote($userData->{'Username'})."
	");
	if (!$sth) {
		return "Database error: ".$dbh->err;
	}

	# Tracking info exists
	if ($sth->rows == 1) {
		# Pull data
		my $data = $sth->fetchrow_hashref();
		$sth->finish();
		# Sanity  check	
		return "Undefined data!" if (ref $data ne "HASH");

		$notifyStatus{'ID'} = $data->{'ID'};
		$notifyStatus{'LastUpdate'} = $data->{'LastUpdate'};
		$notifyStatus{'UpdateInterval'} = $data->{'UpdateInterval'};
		$notifyStatus{'LastValue'} = $data->{'LastValue'};

	# No tracking info
	} elsif ($sth->rows == 0) {
		$sth->finish();
		$notifyStatus{'LastUpdate'} = 0;
		$notifyStatus{'UpdateInterval'} = 86400;

		# Insert record seeing as it doesn't exist
		my $res = $dbh->do("
				INSERT INTO radiusNotifyTrack
					(
						Username,
						LastUpdate,
						UpdateInterval
					)
				VALUES
					(
						".$dbh->quote($userData->{'Username'}).",
						".$dbh->quote($notifyStatus{'LastUpdate'}).",
						".$dbh->quote($notifyStatus{'UpdateInterval'})."
					)
		");
		return "Failed to insert radius tracking data: ".$dbh->err if (!$res);

	# Wth happened here?
	} else {
		my $msg = "Unknown number of rows returned for radius tracking: ".$sth->rows;
		$sth->finish();
		return $msg;
	}

	return \%notifyStatus;
}


# Function to update users notify status
sub updateNotifyStatus
{
	my ($dbh,$userData,$notifyStatus) = @_;


	# Update accounting
	my $res = $dbh->do("
		UPDATE radiusNotifyTrack
		SET
			LastUpdate = ".$dbh->quote($notifyStatus->{'LastUpdate'}).",
			UpdateInterval = ".$dbh->quote($notifyStatus->{'UpdateInterval'})."
		WHERE
			Username = ".$dbh->quote($userData->{'Username'})."
	");
	if (!$res) {
		return "Failed to update notify tracking data: ".$dbh->err;
	}

	return;
}



# Display usage
sub displayUsage {
	print("Usage: $0 [--quiet]\n");
	exit 0;
} 


# vim: ts=4