diff --git a/FEATURES b/FEATURES index 126acc794c4e529e45c89acd32089e396f57417e..8c7a2efebdf4d5dd24f52d33ff76cac862e118af 100644 --- a/FEATURES +++ b/FEATURES @@ -27,7 +27,7 @@ Enhanced features: * Plugin: Topups * Plugin: Auto-topups * Plugin: Usage/Time caps -* Plugin: Prepaid accounting based on usage/time +* Plugin: Prepaid accounting based on usage/time * Plugin: Creation of accounting START records when no START record has been received but an interim update has - helps on slow/lossly links * Plugin: Notifications, % based or approximate time based * Plugin: User blacklists diff --git a/TODO b/TODO index 15e95f71fda64a80bcaf9872d79bcbe013c36a9e..2031e383cab9a6603d54299979f131d5456cd484 100644 --- a/TODO +++ b/TODO @@ -5,8 +5,7 @@ smradiusd: * Create a raddbpath config option which is prepended to dict paths -usage related queries: -* Use Math module to perform calculations +* Configurable 'use defaults for POD/CoA' we may not want to send these smadmin: * Ability to run smadmin before the end of current month and updating the records as necessary at a later stage diff --git a/database/core.tsql b/database/core.tsql index b6478aafec3d71bd330f6570bcadfc39b795d501..f5222e4ae58013d1e9d50937914f058e371cf6ea 100644 --- a/database/core.tsql +++ b/database/core.tsql @@ -199,13 +199,13 @@ CREATE TABLE @PREFIX@accounting ( ServiceType @INT_UNSIGNED@, - FramedProtocol @INT_UNSIGNED@, + FramedProtocol @INT_UNSIGNED@, NASPort VARCHAR(255), NASPortType @INT_UNSIGNED@, - CallingStationID VARCHAR(255), + CallingStationID VARCHAR(255), CalledStationID VARCHAR(255), @@ -251,6 +251,9 @@ CREATE INDEX @PREFIX@accounting_idx2 ON @PREFIX@accounting (PeriodKey); CREATE INDEX @PREFIX@accounting_idx4 ON @PREFIX@accounting (Username,AcctSessionID,NASIPAddress,NASPort); /* accounting_update_query */ CREATE INDEX @PREFIX@accounting_idx5 ON @PREFIX@accounting (Username,AcctSessionID,NASIPAddress,NASPort,PeriodKey); +/* Index for the EventTimestamp */ +CREATE INDEX @PREFIX@accounting_idx7 ON @PREFIX@accounting (EventTimestamp); +CREATE INDEX @PREFIX@accounting_idx8 ON @PREFIX@accounting (Username,EventTimestamp); @@ -276,7 +279,7 @@ CREATE INDEX @PREFIX@accounting_summary_idx3 ON @PREFIX@accounting_summary (User /* Users data */ CREATE TABLE @PREFIX@users_data ( - ID @SERIAL_TYPE@, + ID @SERIAL_TYPE@, UserID @INT_UNSIGNED@, @@ -287,4 +290,4 @@ CREATE TABLE @PREFIX@users_data ( Value VARCHAR(255), UNIQUE (UserID,Name) -) @CREATE_TABLE_SUFFIX@; +) @CREATE_TABLE_SUFFIX@; diff --git a/lib/smradius/attributes.pm b/lib/smradius/attributes.pm index f72fb50ac95e72f4af92cff043ee556446f74248..f1a8b7eda7202f3e4675945193f5cf9d447718fb 100644 --- a/lib/smradius/attributes.pm +++ b/lib/smradius/attributes.pm @@ -81,6 +81,10 @@ my @attributeReplyIgnoreList = ( 'SMRadius-AutoTopup-Uptime-Notify', 'SMRadius-AutoTopup-Uptime-NotifyTemplate', 'SMRadius-AutoTopup-Uptime-Threshold', + 'SMRadius-Config-Filter-Reply-Attribute', + 'SMRadius-Config-Filter-Reply-VAttribute', + 'SMRadius-FUP-Period', + 'SMRadius-FUP-Traffic-Threshold', ); my @attributeVReplyIgnoreList = ( ); @@ -342,7 +346,7 @@ sub checkAuthAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. } elsif ($operator eq '+=') { @@ -439,7 +443,7 @@ sub checkAcctAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. if ($operator eq '+=') { @@ -546,7 +550,7 @@ sub setReplyAttribute # Always matches as a check item, and replaces in the configuration items any attribute of the same name. # If no attribute of that name appears in the request, then this attribute is added. # - # As a reply item, it has an itendtical meaning, but for the reply items, instead of the request items. + # As a reply item, it has an idendtical meaning, but for the reply items, instead of the request items. } elsif ($attribute->{'Operator'} eq ':=') { # Overwrite @@ -561,7 +565,7 @@ sub setReplyAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. } elsif ($attribute->{'Operator'} eq '+=') { @@ -610,7 +614,7 @@ sub setReplyVAttribute @attrValues = ( $attribute->{'Value'} ); } - $server->log(LOG_DEBUG,"[VATTRIBUTES] Processing REPLY attribute: '". + $server->log(LOG_DEBUG,"[VATTRIBUTES] Processing REPLY vattribute: '". $attribute->{'Name'}."' ".$attribute->{'Operator'}." '".join("','",@attrValues)."'"); @@ -640,7 +644,7 @@ sub setReplyVAttribute # Always matches as a check item, and replaces in the configuration items any attribute of the same name. # If no attribute of that name appears in the request, then this attribute is added. # - # As a reply item, it has an itendtical meaning, but for the reply items, instead of the request items. + # As a reply item, it has an idendtical meaning, but for the reply items, instead of the request items. } elsif ($attribute->{'Operator'} eq ':=') { # Overwrite @@ -655,7 +659,7 @@ sub setReplyVAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. } elsif ($attribute->{'Operator'} eq '+=') { @@ -707,7 +711,7 @@ sub processConfigAttribute # Always matches as a check item, and adds the current # attribute with value to the list of configuration items. # - # As a reply item, it has an itendtical meaning, but the + # As a reply item, it has an idendtical meaning, but the # attribute is added to the reply items. if ($attribute->{'Operator'} eq '+=') { @@ -720,7 +724,7 @@ sub processConfigAttribute # Always matches as a check item, and replaces in the configuration items any attribute of the same name. # If no attribute of that name appears in the request, then this attribute is added. # - # As a reply item, it has an itendtical meaning, but for the reply items, instead of the request items. + # As a reply item, it has an idendtical meaning, but for the reply items, instead of the request items. } elsif ($attribute->{'Operator'} eq ':=') { @{$configAttributes->{$attribute->{'Name'}}} = @attrValues; @@ -790,7 +794,8 @@ sub processConditional # Split off expression - my ($condition,$onTrue,$onFalse) = ($attrVal =~ /^([^\?]*)(?:\?\s*((?:\S+)?[^:]*)(?:\s*\:\s*(.*))?)?$/); + # NK: This probably needs a bit of work + my ($condition,$onTrue,$onFalse) = ($attrVal =~ /^([^\?]*)(?:\?\s*((?:\S+)?[^:]*)(?:\:\s*(.*))?)?$/); # If there is no condition we cannot really continue? if (!defined($condition)) { @@ -838,6 +843,10 @@ sub processConditional $res = 1; } + # Sanitize the output + $attribStr =~ s/^\s*//; + $attribStr =~ s/\s*$//; + $server->log(LOG_DEBUG,"[ATTRIBUTES] - Evaluated to '$res' returning '".(defined($attribStr) ? $attribStr : "-undef-")."'"); # Loop with attributes: diff --git a/lib/smradius/client.pm b/lib/smradius/client.pm index 8248ff6894f321b4a3d516bd4a6bce791edfe27a..0ea5a264c4cb26d50be5fd0eca333056032e05e7 100644 --- a/lib/smradius/client.pm +++ b/lib/smradius/client.pm @@ -1,5 +1,5 @@ # Radius client -# Copyright (C) 2007-2016, AllWorldIT +# Copyright (C) 2007-2019, AllWorldIT # # 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 @@ -44,11 +44,15 @@ if (!eval {require Config::IniFiles; 1;}) { sub run { my ($self,@methodArgs) = @_; + + # Instantiate if we're not already instantiated $self = $self->new() if (!ref($self)); + # The hash we're going to return + my $ret = { }; - print(STDERR "SMRadClient v$VERSION - Copyright (c) 2007-2016, AllWorldIT\n"); + print(STDERR "SMRadClient v".VERSION." - Copyright (c) 2007-2019, AllWorldIT\n"); print(STDERR "\n"); @@ -67,10 +71,11 @@ sub run \%{$cmdline}, "config:s", "raddb:s", + "listen:s", "help", )) { - print(STDERR "ERROR: Error parsing commandline arguments"); - return 1; + print(STDERR "ERROR: Error parsing commandline arguments"); + return 1; } # Check for some args @@ -190,13 +195,38 @@ sub run return 1; } + my $sock2; + # Check if we must listen on another IP/port + if (defined($cmdline->{'listen'}) && $cmdline->{'listen'} ne "") { + print(STDERR "Creating second socket\n"); + + # Check the details we were provided + my ($localAddr,$localPort) = split(/:/,$cmdline->{'listen'}); + if (!defined($localPort)) { + print(STDERR "ERROR: The format for --listen is IP:Port\n"); + return 1; + } + + $sock2 = IO::Socket::INET->new( + LocalAddr => $localAddr, + LocalPort => $localPort, + Type => SOCK_DGRAM, + Proto => 'udp', + Timeout => $sockTimeout, + ); + + if (!$sock2) { + print(STDERR "ERROR: Failed to create second socket\n"); + return 1; + } + } + # Check if we sent the packet... if (!$sock->send($udp_packet)) { print(STDERR "ERROR: Failed to send data on socket\n"); return 1; } - # And time for the response print(STDERR "\nResponse:\n"); @@ -216,7 +246,7 @@ sub run # Read packet $sock->recv($udp_packet, 65536); if (!$udp_packet) { - print(STDERR "ERROR: Receive response data failed: $!\n"); + print(STDERR "ERROR: Receive response data failed on socket: $!\n"); return 1; } @@ -225,13 +255,44 @@ sub run print(STDERR " > Authenticated: ". (defined(auth_req_verify($udp_packet,$self->{'secret'},$authen)) ? "yes" : "no") ."\n"); print(STDERR $pkt->str_dump()); + # Setup response + $ret->{'request'} = $self->hashedPacket($self->{'packet'}); + $ret->{'response'} = $self->hashedPacket($pkt); + + + my $udp_packet2; + if (defined($sock2)) { + my $rsock2 = IO::Select->new($sock2); + if (!$rsock2) { + print(STDERR "ERROR: Failed to select response data on socket2\n"); + return 1; + } + + # Check if we can read a response after the select() + if (!$rsock2->can_read($sockTimeout)) { + print(STDERR "ERROR: Failed to receive response data on socket2\n"); + return 1; + } + + # Read packet + my $udp_packet2; + $sock2->recv($udp_packet2, 65536); + if (!$udp_packet2) { + print(STDERR "ERROR: Receive response data failed on socket2: $!\n"); + return 1; + } + + my $pkt2 = smradius::Radius::Packet->new($raddb,$udp_packet2); + print(STDERR $pkt2->str_dump()); + + # Save the packet we got + $ret->{'listen'}->{'response'} = $self->hashedPacket($pkt2); + } + # If we were called as a function, return hashed version of the response packet if (@methodArgs) { - return { - 'request' => $self->hashedPacket($self->{'packet'}), - 'response' => $self->hashedPacket($pkt), - }; + return $ret; } return 0; @@ -261,7 +322,7 @@ sub hashedPacket foreach my $attrName ($pkt->vsattributes($attrVendor)) { $res->{'vattributes'}->{$attrVendor}->{$attrName} = $pkt->vsattr($attrVendor,$attrName); } - } + } return $res; } diff --git a/lib/smradius/constants.pm b/lib/smradius/constants.pm index b02f2f88c6813ee31f2c5ff44d2c6b2447e1646c..b23ddf983a70171648aeaa6e85e90f555bfca119 100644 --- a/lib/smradius/constants.pm +++ b/lib/smradius/constants.pm @@ -20,14 +20,13 @@ ## @class smradius::constants # SMRadius constants package package smradius::constants; +use base qw(Exporter); use strict; use warnings; -# Exporter stuff -use base qw(Exporter); our (@EXPORT,@EXPORT_OK); @EXPORT = qw( RES_OK @@ -37,7 +36,7 @@ our (@EXPORT,@EXPORT_OK); MOD_RES_NACK MOD_RES_SKIP - UINT_MAX + GIGAWORD_VALUE ); @EXPORT_OK = (); @@ -50,7 +49,7 @@ use constant { MOD_RES_ACK => 1, MOD_RES_NACK => 2, - UINT_MAX => 2**32 + GIGAWORD_VALUE => 2**32, }; diff --git a/lib/smradius/daemon.pm b/lib/smradius/daemon.pm index e3486fd0912ccef8ea56ee4418a1245f4e6e254e..7775751daac63e2be24b65d2a000396c2d6b9930 100644 --- a/lib/smradius/daemon.pm +++ b/lib/smradius/daemon.pm @@ -1,5 +1,5 @@ # Radius daemon -# Copyright (C) 2007-2016, AllWorldIT +# Copyright (C) 2007-2019, AllWorldIT # # 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 @@ -898,109 +898,145 @@ sub process_request { foreach my $attrOp (keys %{$user->{'Attributes'}->{$attrName}}) { # Grab attribute my $attr = $user->{'Attributes'}->{$attrName}->{$attrOp}; - # Check attribute against accounting attributes attributes + # Check attribute against accounting attributes my $res = checkAcctAttribute($self,$user,$acctAttributes,$attr); # We don't care if it fails } } - # Check if we must POD the user + # The coaReq may be either POD or CoA + my $coaReq = smradius::Radius::Packet->new($self->{'radius'}->{'dictionary'}); + + # Set packet identifier + $coaReq->set_identifier( $$ & 0xff ); + + # Check if we must POD the user, if so we set the code to disconnect if ($PODUser) { $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Trying to disconnect user..."); + $coaReq->set_code('Disconnect-Request'); + } else { + # If this is *not* a POD, we need to process reply attributes + $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Sending CoA..."); + $coaReq->set_code('CoA-Request'); + # Process the reply attributes + $self->_processReplyAttributes($request,$user,$coaReq); + } - my $resp = smradius::Radius::Packet->new($self->{'radius'}->{'dictionary'}); + # NAS identification + $coaReq->set_attr('NAS-IP-Address',$pkt->attr('NAS-IP-Address')); + # Session identification + $coaReq->set_attr('User-Name',$pkt->attr('User-Name')); + $coaReq->set_attr('NAS-Port',$pkt->attr('NAS-Port')); + $coaReq->set_attr('Acct-Session-Id',$pkt->attr('Acct-Session-Id')); - $resp->set_code('Disconnect-Request'); - my $id = $$ & 0xff; - $resp->set_identifier( $id ); - - $resp->set_attr('User-Name',$pkt->attr('User-Name')); - $resp->set_attr('Framed-IP-Address',$pkt->attr('Framed-IP-Address')); - $resp->set_attr('NAS-IP-Address',$pkt->attr('NAS-IP-Address')); - - # Add onto logline - $request->addLogLine(". REPLY => "); - foreach my $attrName ($resp->attributes) { - $request->addLogLine( - "%s: '%s'", - $attrName, - $resp->rawattr($attrName) - ); + # Add onto logline + $request->addLogLine(". REPLY => "); + foreach my $attrName ($coaReq->attributes) { + $request->addLogLine( + "%s: '%s'", + $attrName, + $coaReq->rawattr($attrName) + ); + } + + # Generate coaReq packet + my $coaReq_packet = auth_resp($coaReq->pack, getAttributeValue($user->{'ConfigAttributes'},"SMRadius-Config-Secret")); + + # Array CoA servers to contact + my @coaServers; + + # Check for old POD server attribute + if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'})) { + $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-PODServer is defined"); + @coaServers = @{$user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'}}; + } + + # Check for new CoA server attribute + if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-CoAServer'})) { + $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-CoAServer is defined"); + @coaServers = @{$user->{'ConfigAttributes'}->{'SMRadius-Config-CoAServer'}}; + } + + # If we didn't get provided a CoA server, use the peer address + if (!@coaServers) { + push(@coaServers,$server->{'peeraddr'}); + } + + # Check address format + foreach my $coaServer (@coaServers) { + # Remove IPv6 portion for now... + $coaServer =~ s/^::ffff://; + # Check for valid IP + my ($coaServerIP,$coaServerPort) = ($coaServer =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})(?::([0-9]+))?/); + + if (!defined($coaServerIP)) { + $self->log(LOG_NOTICE,"[SMRADIUS] POST-ACCT: CoAServer '$coaServer' looks incorrect"); + next; } - # Grab packet - my $response = auth_resp($resp->pack, getAttributeValue($user->{'ConfigAttributes'},"SMRadius-Config-Secret")); - - # Check for POD Servers and send disconnect - if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'})) { - $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-PODServer is defined"); - - # Check address format - foreach my $podServerAttribute (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-PODServer'}}) { - # Check for valid IP - if ($podServerAttribute =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})/) { - my $podServer = $1; - - # If we have a port, use it, otherwise use default 1700 - my $podServerPort; - if ($podServerAttribute =~ /:([0-9]+)$/) { - $podServerPort = $1; - } else { - $podServerPort = 1700; - } - - $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Trying PODServer => IP: '".$podServer."' Port: '".$podServerPort."'"); - - # Create socket to send packet out on - my $podServerTimeout = "10"; # 10 second timeout - my $podSock = IO::Socket::INET->new( - PeerAddr => $podServer, - PeerPort => $podServerPort, - Type => SOCK_DGRAM, - Proto => 'udp', - TimeOut => $podServerTimeout, - ); - - if (!$podSock) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to create socket to send POD on"); - next; - } - - # Check if we sent the packet... - if (!$podSock->send($response)) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to send data on socket"); - next; - } - - # Once sent, we need to get a response back - my $sh = IO::Select->new($podSock); - if (!$sh) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to select data on socket"); - next; - } - - if (!$sh->can_read($podServerTimeout)) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to receive data on socket"); - next; - } - - my $data; - $podSock->recv($data, 65536); - if (!$data) { - $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Receive data failed"); - $logReason = "POD Failure"; - } else { - $logReason = "User POD"; - } - - #my @stuff = unpack('C C n a16 a*', $data); - #$self->log(LOG_DEBUG,"STUFF: ".Dumper(\@stuff)); - } else { - $self->log(LOG_DEBUG,"[SMRADIUS] Invalid POD Server value: '".$podServerAttribute."'"); - } - } + # Set default CoA server port + $coaServerPort //= 1700; + + $self->log(LOG_DEBUG,"[SMRADIUS] POST-ACCT: Trying CoAServer => IP: '".$coaServer."' Port: '".$coaServerPort."'"); + + # Create socket to send packet out on + my $coaServerTimeout = "2"; # 2 second timeout + my $coaSock = IO::Socket::INET->new( + PeerAddr => $coaServerIP, + PeerPort => $coaServerPort, + Type => SOCK_DGRAM, + Proto => 'udp', + TimeOut => $coaServerTimeout, + ); + + if (!$coaSock) { + $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to create socket to send CoA on: $!"); + next; + } + + # Check if we sent the packet... + if (!$coaSock->send($coaReq_packet)) { + $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to send data on CoA socket: $!"); + next; + } + + # Once sent, we need to get a response back + my $select = IO::Select->new($coaSock); + if (!$select) { + $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to select data on socket: $!"); + next; + } + + if (!$select->can_read($coaServerTimeout)) { + $self->log(LOG_ERR,"[SMRADIUS] POST-ACCT: Failed to receive data on socket: $!"); + next; + } + + # Grab CoA response + my $coaRes_packet; + $coaSock->recv($coaRes_packet, 65536); + if (!$coaRes_packet) { + $self->log(LOG_INFO,"[SMRADIUS] POST-ACCT: No data received in response to our request to '$coaServerIP:$coaServerPort': $!"); + $logReason = "No Response"; + next; + } + + # Parse the radius packet + my $coaRes = smradius::Radius::Packet->new($self->{'radius'}->{'dictionary'},$coaRes_packet); + + # Check status + if ($coaRes->code eq "CoA-ACK") { + $logReason = "CoA Success"; + last; + } elsif ($coaRes->code eq "CoA-NACK") { + $logReason = "CoA Fail"; + } elsif ($coaRes->code eq "Disconnect-ACK") { + $logReason = "POD Success"; + last; + } elsif ($coaRes->code eq "Disconnect-NACK") { + $logReason = "POD Fail"; } else { - $self->log(LOG_DEBUG,"[SMRADIUS] SMRadius-Config-PODServer is not defined"); + $logReason = "CoA/POD Fail"; } } @@ -1154,133 +1190,8 @@ sub process_request { $resp->set_identifier($pkt->identifier); $resp->set_authenticator($pkt->authenticator); - # Loop with attributes we got from the getReplyAttributes function, its a hash of arrays which are the values - my %replyAttributes = %{ $user->{'ReplyAttributes'} }; - foreach my $attrName (keys %{$user->{'Attributes'}}) { - # Loop with operators - foreach my $attrOp (keys %{$user->{'Attributes'}->{$attrName}}) { - # Grab attribute - my $attr = $user->{'Attributes'}->{$attrName}->{$attrOp}; - # Add this to the reply attribute? - setReplyAttribute($self,\%replyAttributes,$attr); - } - } - # Loop with reply attributes - $request->addLogLine(". RFILTER => "); - foreach my $attrName (keys %replyAttributes) { - # Loop with values - foreach my $value (@{$replyAttributes{$attrName}}) { - # Check for filter matches - my $excluded = 0; - foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-Attribute'}}) { - my @attrList = split(/[;,]/,$item); - foreach my $aItem (@attrList) { - $excluded = 1 if (lc($attrName) eq lc($aItem)); - } - } - # If we must be filtered, just exclude it then - if (!$excluded) { - # Add each value - $resp->set_attr($attrName,$value); - } else { - $request->addLogLine("%s ",$attrName); - } - } - } - # Loop with vendor reply attributes - $request->addLogLine(". RVFILTER => "); - my %replyVAttributes = (); - # Process reply vattributes already added - foreach my $vendor (keys %{ $user->{'ReplyVAttributes'} }) { - # Loop with operators - foreach my $attrName (keys %{$user->{'ReplyVAttributes'}->{$vendor}}) { - # Add each value - foreach my $value (@{$user->{'ReplyVAttributes'}{$vendor}->{$attrName}}) { - # Check for filter matches - my $excluded = 0; - foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-VAttribute'}}) { - my @attrList = split(/[;,]/,$item); - foreach my $aItem (@attrList) { - $excluded = 1 if (lc($attrName) eq lc($aItem)); - } - } - # If we must be filtered, just exclude it then - if (!$excluded) { - # This attribute is not excluded, so its ok - $replyVAttributes{$vendor}->{$attrName} = $user->{'ReplyVAttributes'}->{$vendor}->{$attrName}; - } else { - $request->addLogLine("%s ",$attrName); - } - } - } - } - # Process VAttributes - foreach my $attrName (keys %{$user->{'VAttributes'}}) { - # Loop with operators - foreach my $attrOp (keys %{$user->{'VAttributes'}->{$attrName}}) { - # Check for filter matches - my $excluded = 0; - foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-VAttribute'}}) { - my @attrList = split(/[;,]/,$item); - foreach my $aItem (@attrList) { - $excluded = 1 if (lc($attrName) eq lc($aItem)); - } - } - # If we must be filtered, just exclude it then - if (!$excluded) { - # Grab attribute - my $attr = $user->{'VAttributes'}->{$attrName}->{$attrOp}; - # Add this to the reply attribute? - setReplyVAttribute($self,\%replyVAttributes,$attr); - } else { - $request->addLogLine("%s ",$attrName); - } - } - } - foreach my $vendor (keys %replyVAttributes) { - # Loop with operators - foreach my $attrName (keys %{$replyVAttributes{$vendor}}) { - # Add each value - foreach my $value (@{$replyVAttributes{$vendor}->{$attrName}}) { - $resp->set_vsattr($vendor,$attrName,$value); - } - } - } - - # Add attributes onto logline - $request->addLogLine(". REPLY => "); - foreach my $attrName ($resp->attributes) { - $request->addLogLine( - "%s: '%s", - $attrName, - $resp->rawattr($attrName) - ); - } - - # Add vattributes onto logline - $request->addLogLine(". VREPLY => "); - # Loop with vendors - foreach my $vendor ($resp->vendors()) { - # Loop with attributes - foreach my $attrName ($resp->vsattributes($vendor)) { - # Grab the value - my @attrRawVal = ( $resp->vsattr($vendor,$attrName) ); - my $attrVal = $attrRawVal[0][0]; - # Sanatize it a bit - if ($attrVal =~ /[[:cntrl:]]/) { - $attrVal = "-nonprint-"; - } else { - $attrVal = "'$attrVal'"; - } - - $request->addLogLine( - "%s/%s: %s", - $vendor, - $attrName, - $attrVal - ); - } - } + # Process the reply attributes + $self->_processReplyAttributes($request,$user,$resp); $server->{'client'}->send( auth_resp($resp->pack, getAttributeValue($user->{'ConfigAttributes'},"SMRadius-Config-Secret")) @@ -1412,6 +1323,127 @@ EOF +# +# Internal functions +# + + +# Process reply attributes +sub _processReplyAttributes +{ + my ($self,$request,$user,$pkt) = @_; + + # Add attributes we got from plugins and process attributes attached to the user + my %replyAttributes = %{ $user->{'ReplyAttributes'} }; + foreach my $attrName (keys %{$user->{'Attributes'}}) { + # Loop with operators + foreach my $attrOp (keys %{$user->{'Attributes'}->{$attrName}}) { + # Grab attribute + my $attr = $user->{'Attributes'}->{$attrName}->{$attrOp}; + # Add this to the reply attribute? + setReplyAttribute($self,\%replyAttributes,$attr); + } + } + # Add vendor attributes we got from plugins and process attributes attached to the user + my %replyVAttributes = %{ $user->{'ReplyVAttributes'} }; + foreach my $attrName (keys %{$user->{'VAttributes'}}) { + # Loop with operators + foreach my $attrOp (keys %{$user->{'VAttributes'}->{$attrName}}) { + # Grab attribute + my $attr = $user->{'VAttributes'}->{$attrName}->{$attrOp}; + # Add this to the reply attribute? + setReplyVAttribute($self,\%replyVAttributes,$attr); + } + } + + # Loop with reply attributes add them to our response, or output them to log if they were excluded + $request->addLogLine("RFILTER => "); + foreach my $attrName (keys %replyAttributes) { + # Loop with values + foreach my $value (@{$replyAttributes{$attrName}}) { + # Check for filter matches + my $excluded = 0; + foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-Attribute'}}) { + my @attrList = split(/[;,]/,$item); + foreach my $aItem (@attrList) { + $excluded = 1 if (lc($attrName) eq lc($aItem)); + } + } + # If we must be filtered, just exclude it then + if (!$excluded) { + # Add each value + $pkt->set_attr($attrName,$value); + } else { + $request->addLogLine("%s ",$attrName); + } + } + } + + # Loop with reply vendor attributes add them to our response, or output them to log if they were excluded + $request->addLogLine(". RVFILTER => "); + # Process reply vattributes already added + foreach my $vendor (keys %replyVAttributes) { + # Loop with operators + foreach my $attrName (keys %{$replyVAttributes{$vendor}}) { + # Add each value + foreach my $value (@{$replyVAttributes{$vendor}->{$attrName}}) { + # Check for filter matches + my $excluded = 0; + foreach my $item (@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Filter-Reply-VAttribute'}}) { + my @attrList = split(/[;,]/,$item); + foreach my $aItem (@attrList) { + $excluded = 1 if (lc($attrName) eq lc($aItem)); + } + } + # If we must be filtered, just exclude it then + if (!$excluded) { + # This attribute is not excluded, so its ok + $pkt->set_vsattr($vendor,$attrName,$value); + } else { + $request->addLogLine("%s ",$attrName); + } + } + } + } + + # Add attributes onto logline + $request->addLogLine(". REPLY => "); + foreach my $attrName ($pkt->attributes) { + $request->addLogLine( + "%s: '%s", + $attrName, + $pkt->rawattr($attrName) + ); + } + # Add vattributes onto logline + $request->addLogLine(". VREPLY => "); + # Loop with vendors + foreach my $vendor ($pkt->vendors()) { + # Loop with attributes + foreach my $attrName ($pkt->vsattributes($vendor)) { + # Grab the value + my @attrRawVal = ( $pkt->vsattr($vendor,$attrName) ); + my $attrVal = $attrRawVal[0][0]; + # Sanatize it a bit + if ($attrVal =~ /[[:cntrl:]]/) { + $attrVal = "-nonprint-"; + } else { + $attrVal = "'$attrVal'"; + } + $request->addLogLine( + "%s/%s: %s", + $vendor, + $attrName, + $attrVal + ); + } + } + + return $self; +}; + + + 1; # vim: ts=4 diff --git a/lib/smradius/modules/accounting/mod_accounting_sql.pm b/lib/smradius/modules/accounting/mod_accounting_sql.pm index 98976aa3c65ea0024bef33358557eb49f4c9f269..d3265d16b1b56dfc7f12995c6fd36eae82457f47 100644 --- a/lib/smradius/modules/accounting/mod_accounting_sql.pm +++ b/lib/smradius/modules/accounting/mod_accounting_sql.pm @@ -1,5 +1,5 @@ # SQL accounting database -# Copyright (C) 2007-2016, AllWorldIT +# Copyright (C) 2007-2019, AllWorldIT # # 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 @@ -206,6 +206,20 @@ sub init AND PeriodKey = %{query.PeriodKey} '; + $config->{'accounting_usage_query_period'} = ' + SELECT + SUM(AcctInputOctets) AS AcctInputOctets, + SUM(AcctOutputOctets) AS AcctOutputOctets, + SUM(AcctInputGigawords) AS AcctInputGigawords, + SUM(AcctOutputGigawords) AS AcctOutputGigawords, + SUM(AcctSessionTime) AS AcctSessionTime + FROM + @TP@accounting + WHERE + Username = %{user.Username} + AND EventTimestamp > %{query.PeriodKey} + '; + $config->{'accounting_select_duplicates_query'} = ' SELECT ID @@ -280,6 +294,15 @@ sub init $config->{'accounting_usage_query'} = $scfg->{'mod_accounting_sql'}->{'accounting_usage_query'}; } } + if (defined($scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'}) && + $scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'} ne "") { + if (ref($scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'}) eq "ARRAY") { + $config->{'accounting_usage_query_period'} = join(' ', + @{$scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'}}); + } else { + $config->{'accounting_usage_query_period'} = $scfg->{'mod_accounting_sql'}->{'accounting_usage_query_period'}; + } + } if (defined($scfg->{'mod_accounting_sql'}->{'accounting_select_duplicates_query'}) && $scfg->{'mod_accounting_sql'}->{'accounting_select_duplicates_query'} ne "") { if (ref($scfg->{'mod_accounting_sql'}->{'accounting_select_duplicates_query'}) eq "ARRAY") { @@ -327,9 +350,10 @@ sub init # Function to get radius user data usage +# The 'period' parameter is optional and is the number of days to return usage for sub getUsage { - my ($server,$user,$packet) = @_; + my ($server,$user,$packet,$period) = @_; # Build template my $template; @@ -341,9 +365,27 @@ sub getUsage $template->{'user'}->{'ID'} = $user->{'ID'}; $template->{'user'}->{'Username'} = $user->{'Username'}; - # Current PeriodKey + # Current PeriodKey, this is used for non-$period queries my $now = DateTime->now->set_time_zone($server->{'smradius'}->{'event_timezone'}); - $template->{'query'}->{'PeriodKey'} = $now->strftime("%Y-%m"); + + # Query template to use below + my $queryTemplate; + # If we're doing a query for a specific period + if (defined($period)) { + # We need to switch out the query to the period query + $queryTemplate = "accounting_usage_query_period"; + # Grab a clone of now, and create the start date DateTime object + my $startDate = $now->clone->subtract( 'days' => $period ); + # And we add the start date + $template->{'query'}->{'PeriodKey'} = $startDate->ymd(); + + # If not, we just use PeriodKey as normal... + } else { + # Set the normal PeriodKey query template to use + $queryTemplate = "accounting_usage_query"; + # And set the period key to this month + $template->{'query'}->{'PeriodKey'} = $now->strftime("%Y-%m"); + } # If we using caching, check how old the result is if (defined($config->{'accounting_usage_cache_time'})) { @@ -355,7 +397,7 @@ sub getUsage } # Replace template entries - my (@dbDoParams) = templateReplace($config->{'accounting_usage_query'},$template); + my (@dbDoParams) = templateReplace($config->{$queryTemplate},$template); # Fetch data my $sth = DBSelect(@dbDoParams); @@ -366,9 +408,9 @@ sub getUsage # Our usage hash my %usageTotals; - $usageTotals{'TotalSessionTime'} = Math::BigInt->new(); - $usageTotals{'TotalDataInput'} = Math::BigInt->new(); - $usageTotals{'TotalDataOutput'} = Math::BigInt->new(); + $usageTotals{'TotalSessionTime'} = Math::BigInt->new(0); + $usageTotals{'TotalDataInput'} = Math::BigInt->new(0); + $usageTotals{'TotalDataOutput'} = Math::BigInt->new(0); # Pull in usage and add up while (my $row = hashifyLCtoMC($sth->fetchrow_hashref(), @@ -385,7 +427,7 @@ sub getUsage } if (defined($row->{'AcctInputGigawords'}) && $row->{'AcctInputGigawords'} > 0) { my $inputGigawords = Math::BigInt->new($row->{'AcctInputGigawords'}); - $inputGigawords->bmul(UINT_MAX); + $inputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{'TotalDataInput'}->badd($inputGigawords); } # Add output usage if we have any @@ -394,16 +436,16 @@ sub getUsage } if (defined($row->{'AcctOutputGigawords'}) && $row->{'AcctOutputGigawords'} > 0) { my $outputGigawords = Math::BigInt->new($row->{'AcctOutputGigawords'}); - $outputGigawords->bmul(UINT_MAX); + $outputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{'TotalDataOutput'}->badd($outputGigawords); } } DBFreeRes($sth); # Convert to bigfloat for accuracy - my $totalData = Math::BigFloat->new(); + my $totalData = Math::BigFloat->new(0); $totalData->badd($usageTotals{'TotalDataOutput'})->badd($usageTotals{'TotalDataInput'}); - my $totalTime = Math::BigFloat->new(); + my $totalTime = Math::BigFloat->new(0); $totalTime->badd($usageTotals{'TotalSessionTime'}); # Rounding up @@ -472,12 +514,13 @@ sub acct_log return; } - # Convert session total gigawords/octets into bytes - my $totalInputBytes = Math::BigInt->new(); - $totalInputBytes->badd($template->{'request'}->{'Acct-Input-Gigawords'})->bmul(UINT_MAX); + # Convert session total gigawords into bytes + my $totalInputBytes = Math::BigInt->new($template->{'request'}->{'Acct-Input-Gigawords'}); + my $totalOutputBytes = Math::BigInt->new($template->{'request'}->{'Acct-Output-Gigawords'}); + $totalInputBytes->bmul(GIGAWORD_VALUE); + $totalOutputBytes->bmul(GIGAWORD_VALUE); + # Add byte counters $totalInputBytes->badd($template->{'request'}->{'Acct-Input-Octets'}); - my $totalOutputBytes = Math::BigInt->new(); - $totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Gigawords'})->bmul(UINT_MAX); $totalOutputBytes->badd($template->{'request'}->{'Acct-Output-Octets'}); # Packets, no conversion my $totalInputPackets = Math::BigInt->new($template->{'request'}->{'Acct-Input-Packets'}); @@ -490,13 +533,22 @@ sub acct_log qw(AcctInputOctets AcctInputPackets AcctOutputOctets AcctOutputPackets AcctInputGigawords AcctOutputGigawords SessionTime PeriodKey) )) { - - # Convert this session usage to bytes - my $sessionInputBytes = Math::BigInt->new(); - $sessionInputBytes->badd($sessionPart->{'AcctInputGigawods'})->bmul(UINT_MAX); + # Make sure we treat undef values sort of sanely + $sessionPart->{'AcctInputGigawords'} //= 0; + $sessionPart->{'AcctInputOctets'} //= 0; + $sessionPart->{'AcctOutputGigawords'} //= 0; + $sessionPart->{'AcctOutputOctets'} //= 0; + $sessionPart->{'AcctInputPackets'} //= 0; + $sessionPart->{'AcctOutputPackets'} //= 0; + $sessionPart->{'AcctSessionTime'} //= 0; + + # Convert the gigawords into bytes + my $sessionInputBytes = Math::BigInt->new($sessionPart->{'AcctInputGigawords'}); + my $sessionOutputBytes = Math::BigInt->new($sessionPart->{'AcctOutputGigawords'}); + $sessionInputBytes->bmul(GIGAWORD_VALUE); + $sessionOutputBytes->bmul(GIGAWORD_VALUE); + # Add the byte counters $sessionInputBytes->badd($sessionPart->{'AcctInputOctets'}); - my $sessionOutputBytes = Math::BigInt->new(); - $sessionOutputBytes->badd($sessionPart->{'AcctOutputGigawods'})->bmul(UINT_MAX); $sessionOutputBytes->badd($sessionPart->{'AcctOutputOctets'}); # And packets my $sessionInputPackets = Math::BigInt->new($sessionPart->{'AcctInputPackets'}); @@ -538,8 +590,8 @@ sub acct_log } # Re-calculate - my ($inputGigawordsStr,$inputOctetsStr) = $totalInputBytes->bdiv(UINT_MAX); - my ($outputGigawordsStr,$outputOctetsStr) = $totalOutputBytes->bdiv(UINT_MAX); + my ($inputGigawordsStr,$inputOctetsStr) = $totalInputBytes->bdiv(GIGAWORD_VALUE); + my ($outputGigawordsStr,$outputOctetsStr) = $totalOutputBytes->bdiv(GIGAWORD_VALUE); # Conversion to strings $template->{'query'}->{'Acct-Input-Gigawords'} = $inputGigawordsStr->bstr(); @@ -756,7 +808,7 @@ sub cleanup } if (defined($row->{'AcctInputGigawords'}) && $row->{'AcctInputGigawords'} > 0) { my $inputGigawords = Math::BigInt->new($row->{'AcctInputGigawords'}); - $inputGigawords->bmul(UINT_MAX); + $inputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{$row->{'Username'}}{'TotalDataInput'}->badd($inputGigawords); } # Add output usage if we have any @@ -765,7 +817,7 @@ sub cleanup } if (defined($row->{'AcctOutputGigawords'}) && $row->{'AcctOutputGigawords'} > 0) { my $outputGigawords = Math::BigInt->new($row->{'AcctOutputGigawords'}); - $outputGigawords->bmul(UINT_MAX); + $outputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{$row->{'Username'}}{'TotalDataOutput'}->badd($outputGigawords); } @@ -773,9 +825,9 @@ sub cleanup } else { # Make BigInts for this user - $usageTotals{$row->{'Username'}}{'TotalSessionTime'} = Math::BigInt->new(); - $usageTotals{$row->{'Username'}}{'TotalDataInput'} = Math::BigInt->new(); - $usageTotals{$row->{'Username'}}{'TotalDataOutput'} = Math::BigInt->new(); + $usageTotals{$row->{'Username'}}{'TotalSessionTime'} = Math::BigInt->new(0); + $usageTotals{$row->{'Username'}}{'TotalDataInput'} = Math::BigInt->new(0); + $usageTotals{$row->{'Username'}}{'TotalDataOutput'} = Math::BigInt->new(0); # Look for session time if (defined($row->{'AcctSessionTime'}) && $row->{'AcctSessionTime'} > 0) { @@ -787,7 +839,7 @@ sub cleanup } if (defined($row->{'AcctInputGigawords'}) && $row->{'AcctInputGigawords'} > 0) { my $inputGigawords = Math::BigInt->new($row->{'AcctInputGigawords'}); - $inputGigawords->bmul(UINT_MAX); + $inputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{$row->{'Username'}}{'TotalDataInput'}->badd($inputGigawords); } # Add output usage if we have any @@ -796,7 +848,7 @@ sub cleanup } if (defined($row->{'AcctOutputGigawords'}) && $row->{'AcctOutputGigawords'} > 0) { my $outputGigawords = Math::BigInt->new($row->{'AcctOutputGigawords'}); - $outputGigawords->bmul(UINT_MAX); + $outputGigawords->bmul(GIGAWORD_VALUE); $usageTotals{$row->{'Username'}}{'TotalDataOutput'}->badd($outputGigawords); } diff --git a/lib/smradius/modules/features/mod_feature_capping.pm b/lib/smradius/modules/features/mod_feature_capping.pm index ee25a55ab3b7f528c62bb50d0bc86feff5c7a0b0..b1d1a28244e15c883f4da15b841ccf674f33c5a4 100644 --- a/lib/smradius/modules/features/mod_feature_capping.pm +++ b/lib/smradius/modules/features/mod_feature_capping.pm @@ -1,5 +1,5 @@ # Capping support -# Copyright (C) 2007-2017, AllWorldIT +# Copyright (C) 2007-2019, AllWorldIT # # 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 @@ -219,7 +219,7 @@ sub post_auth_hook # - # Allow for capping overrides by client attribute + # Allow for capping overrides by attribute # if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'})) { @@ -231,7 +231,7 @@ sub post_auth_hook $uptimeLimitWithTopups = $newLimit; $accountingUsage->{'TotalSessionTime'} = $newSessionTime; - $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client uptime multiplier '$multiplier' changes ". + $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] User uptime multiplier '$multiplier' changes ". "uptime limit ('$uptimeLimitWithTopups' => '$newLimit'), ". "uptime usage ('".$accountingUsage->{'TotalSessionTime'}."' => '$newSessionTime')" ); @@ -245,7 +245,7 @@ sub post_auth_hook $trafficLimitWithTopups = $newLimit; $accountingUsage->{'TotalDataUsage'} = $newDataUsage; - $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client traffic multiplier '$multiplier' changes ". + $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] User traffic multiplier '$multiplier' changes ". "traffic limit ('$trafficLimitWithTopups' => '$newLimit'), ". "traffic usage ('".$accountingUsage->{'TotalDataUsage'}."' => '$newDataUsage')" ); @@ -346,7 +346,7 @@ sub post_acct_hook # Skip MAC authentication return MOD_RES_SKIP if ($user->{'_UserDB'}->{'Name'} eq "SQL User Database (MAC authentication)"); - # Exceeding maximum, must be disconnected + # User is either connecting 'START' or disconnecting 'STOP' return MOD_RES_SKIP if ($packet->rawattr('Acct-Status-Type') ne "1" && $packet->rawattr('Acct-Status-Type') ne "3"); $server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] POST ACCT HOOK"); @@ -440,20 +440,20 @@ sub post_acct_hook # - # Allow for capping overrides by client attribute + # Allow for capping overrides by user attribute # if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'})) { my $multiplier = pop(@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'}}); my $newLimit = $uptimeLimitWithTopups * $multiplier; - $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client cap uptime multiplier '$multiplier' changes limit ". + $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] User cap uptime multiplier '$multiplier' changes limit ". "from '$uptimeLimitWithTopups' to '$newLimit'"); $uptimeLimitWithTopups = $newLimit; } if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Traffic-Multiplier'})) { my $multiplier = pop(@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Traffic-Multiplier'}}); my $newLimit = $trafficLimitWithTopups * $multiplier; - $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client cap traffic multiplier '$multiplier' changes limit ". + $server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] User cap traffic multiplier '$multiplier' changes limit ". "from '$trafficLimitWithTopups' to '$newLimit'"); $trafficLimitWithTopups = $newLimit; } @@ -491,7 +491,7 @@ sub post_acct_hook ## @internal -# Code snippet to grab the current uptime limit by processing the user attributes +# Code snippet to grab the current attribute key limit by processing the user attributes sub _getAttributeKeyLimit { my ($server,$user,$attributeKey) = @_; diff --git a/lib/smradius/modules/features/mod_feature_fup.pm b/lib/smradius/modules/features/mod_feature_fup.pm new file mode 100644 index 0000000000000000000000000000000000000000..0f9c91bfc3358b267ea1ee93f5bc895b592256e4 --- /dev/null +++ b/lib/smradius/modules/features/mod_feature_fup.pm @@ -0,0 +1,355 @@ +# FUP support +# Copyright (C) 2007-2019, AllWorldIT +# +# 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. + +package smradius::modules::features::mod_feature_fup; + +use strict; +use warnings; + +# Modules we need +use smradius::attributes; +use smradius::constants; +use smradius::logging; +use smradius::util; + +use AWITPT::Util; +use List::Util qw( min ); +use MIME::Lite; +use POSIX qw( floor ); + + +# Set our version +our $VERSION = "0.0.1"; + + +# Load exporter +use base qw(Exporter); +our @EXPORT = qw( +); +our @EXPORT_OK = qw( +); + + + +# Plugin info +our $pluginInfo = { + Name => "User FUP Feature", + Init => \&init, + + # Authentication hook + 'Feature_Post-Authentication_hook' => \&post_auth_hook, + + # Accounting hook + 'Feature_Post-Accounting_hook' => \&post_acct_hook, +}; + + +# Some constants +my $FUP_PERIOD_ATTRIBUTE = 'SMRadius-FUP-Period'; +my $FUP_TRAFFIC_THRESHOLD_ATTRIBUTE = 'SMRadius-FUP-Traffic-Threshold'; + +my $config; + + + +## @internal +# Initialize module +sub init +{ + my $server = shift; + my $scfg = $server->{'inifile'}; + + + # Defaults + $config->{'enable_mikrotik'} = 0; + + # Setup SQL queries + if (defined($scfg->{'mod_feature_fup'})) { + # Check if option exists + if (defined($scfg->{'mod_feature_fup'}{'enable_mikrotik'})) { + # Pull in config + if (defined(my $val = isBoolean($scfg->{'mod_feature_fup'}{'enable_mikrotik'}))) { + if ($val) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_FUP] Mikrotik-specific vendor return attributes ENABLED"); + $config->{'enable_mikrotik'} = $val; + } + } else { + $server->log(LOG_NOTICE,"[MOD_FEATURE_FUP] Value for 'enable_mikrotik' is invalid"); + } + } + } + + return; +} + + + +## @post_auth_hook($server,$user,$packet) +# Post authentication hook +# +# @param server Server object +# @param user User data +# @param packet Radius packet +# +# @return Result +sub post_auth_hook +{ + my ($server,$user,$packet) = @_; + + + # Skip MAC authentication + return MOD_RES_SKIP if ($user->{'_UserDB'}->{'Name'} eq "SQL User Database (MAC authentication)"); + + $server->log(LOG_DEBUG,"[MOD_FEATURE_FUP] POST AUTH HOOK"); + + + # + # Get threshold from attributes + # + + my $fupPeriod = _getAttributeKeyNumeric($server,$user,$FUP_PERIOD_ATTRIBUTE); + my $trafficThreshold = _getAttributeKeyNumeric($server,$user,$FUP_TRAFFIC_THRESHOLD_ATTRIBUTE); + + # If we have no FUP period, skip + if (!defined($fupPeriod)) { + return MOD_RES_SKIP; + }; + + # If we have no traffic threshold, display an info message and skip + if (!defined($trafficThreshold)) { + $server->log(LOG_INFO,"[MOD_FEATURE_FUP] User has a '$FUP_PERIOD_ATTRIBUTE' defined, but NOT a ". + "'$FUP_TRAFFIC_THRESHOLD_ATTRIBUTE' attribute, aborting FUP checks."); + return MOD_RES_SKIP; + }; + + + # + # Get current traffic and uptime usage + # + + my $accountingUsage = _getAccountingUsage($server,$user,$packet,$fupPeriod); + if (!defined($accountingUsage)) { + return MOD_RES_SKIP; + } + + + # + # Display our FUP info + # + + _logUsage($server,$fupPeriod,$accountingUsage->{'TotalDataUsage'},$trafficThreshold); + + + # + # Check if the user has exceeded the FUP + # + + my $fupExceeded = ($accountingUsage->{'TotalDataUsage'} > $trafficThreshold) ? 1 : 0; + + # + # Add conditional variables + # + + addAttributeConditionalVariable($user,"SMRadius_FUP",$fupExceeded); + + + return MOD_RES_ACK; +} + + + +## @post_acct_hook($server,$user,$packet) +# Post authentication hook +# +# @param server Server object +# @param user User data +# @param packet Radius packet +# +# @return Result +sub post_acct_hook +{ + my ($server,$user,$packet) = @_; + + + # We cannot cap a user if we don't have a UserDB module can we? no userdb, no cap? + return MOD_RES_SKIP if (!defined($user->{'_UserDB'}->{'Name'})); + + # Skip MAC authentication + return MOD_RES_SKIP if ($user->{'_UserDB'}->{'Name'} eq "SQL User Database (MAC authentication)"); + + # User is either connecting 'START' or disconnecting 'STOP' + return MOD_RES_SKIP if ($packet->rawattr('Acct-Status-Type') ne "1" && $packet->rawattr('Acct-Status-Type') ne "3"); + + $server->log(LOG_DEBUG,"[MOD_FEATURE_FUP] POST ACCT HOOK"); + + + # + # Get threshold from attributes + # + + my $fupPeriod = _getAttributeKeyNumeric($server,$user,$FUP_PERIOD_ATTRIBUTE); + my $trafficThreshold = _getAttributeKeyNumeric($server,$user,$FUP_TRAFFIC_THRESHOLD_ATTRIBUTE); + + # If we have no FUP period, skip + if (!defined($fupPeriod)) { + return MOD_RES_SKIP; + }; + + # If we have no traffic threshold, display an info message and skip + if (!defined($trafficThreshold)) { + $server->log(LOG_INFO,"[MOD_FEATURE_FUP] User has a '$FUP_PERIOD_ATTRIBUTE' defined, but NOT a ". + "'$FUP_TRAFFIC_THRESHOLD_ATTRIBUTE' attribute, aborting FUP checks."); + return MOD_RES_SKIP; + }; + + + # + # Get current traffic and uptime usage + # + + my $accountingUsage = _getAccountingUsage($server,$user,$packet,$fupPeriod); + if (!defined($accountingUsage)) { + return MOD_RES_SKIP; + } + + + # + # Display our FUP info + # + + _logUsage($server,$fupPeriod,$accountingUsage->{'TotalDataUsage'},$trafficThreshold); + + + # + # Check if the user has exceeded the FUP + # + + my $fupExceeded = ($accountingUsage->{'TotalDataUsage'} > $trafficThreshold) ? 1 : 0; + + # + # Add conditional variables + # + + addAttributeConditionalVariable($user,"SMRadius_FUP",$fupExceeded); + + + return MOD_RES_ACK; +} + + + +## @internal +# Code snippet to grab the current uptime limit by processing the user attributes +sub _getAttributeKeyNumeric +{ + my ($server,$user,$attributeKey) = @_; + + + # Short circuit return if we don't have the uptime key set + return if (!defined($user->{'Attributes'}->{$attributeKey})); + + # Short circuit if we do not have a valid attribute operator: ':=' + if (!defined($user->{'Attributes'}->{$attributeKey}->{':='})) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_FUP] No valid operators for attribute '". + $user->{'Attributes'}->{$attributeKey}."'"); + return; + } + + $server->log(LOG_DEBUG,"[MOD_FEATURE_FUP] Attribute '".$attributeKey."' is defined"); + + # Check for valid attribute value + if (!defined($user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'}) || + $user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'} !~ /^\d+$/) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_FUP] Attribute '".$user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'}. + "' is NOT a numeric value"); + return; + } + + return $user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'}; +} + + + +## @internal +# Code snippet to grab the accounting usage of a user for a specific period +sub _getAccountingUsage +{ + my ($server,$user,$packet,$period) = @_; + + + foreach my $module (@{$server->{'module_list'}}) { + # Do we have the correct plugin? + if (defined($module->{'Accounting_getUsage'})) { + $server->log(LOG_INFO,"[MOD_FEATURE_FUP] Found plugin: '".$module->{'Name'}."'"); + # Fetch users session uptime & bandwidth used for a specific period + if (my $res = $module->{'Accounting_getUsage'}($server,$user,$packet,$period)) { + return $res; + } + $server->log(LOG_ERR,"[MOD_FEATURE_FUP] No usage data found for user '".$user->{'Username'}."'"); + } + } + + return; +} + + + +## @internal +# Code snippet to log our FUP information +sub _logUsage +{ + my ($server,$period,$total,$threshold) = @_; + + $server->log(LOG_INFO,"[MOD_FEATURE_FUP] FUP information [period: %s days, total: %s, threshold: %s]", + $period,$total,$threshold); + + return; +} + + + +## @internal +# Function snippet to return a attribute +sub _getAttribute +{ + my ($server,$user,$attributeName) = @_; + + + # Check the attribute exists + return if (!defined($user->{'Attributes'}->{$attributeName})); + + $server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] User attribute '".$attributeName."' is defined"); + + # Check the required operator is present in this case := + if (!defined($user->{'Attributes'}->{$attributeName}->{':='})) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] User attribute '".$attributeName."' has no ':=' operator"); + return; + } + + # Check the operator value is defined... + if (!defined($user->{'Attributes'}->{$attributeName}->{':='}->{'Value'})) { + $server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] User attribute '".$attributeName."' has no value"); + return; + } + + return $user->{'Attributes'}->{$attributeName}->{':='}->{'Value'}; +} + + + +1; +# vim: ts=4 diff --git a/lib/smradius/modules/features/mod_feature_user_stats.pm b/lib/smradius/modules/features/mod_feature_user_stats.pm index cce0eb8239bd328833df9710efe2557e9b70e7c3..6fab000fd5cb4037fb4cdd7287fd6bf7ba81e9ca 100644 --- a/lib/smradius/modules/features/mod_feature_user_stats.pm +++ b/lib/smradius/modules/features/mod_feature_user_stats.pm @@ -147,6 +147,23 @@ sub updateUserStats return MOD_RES_SKIP; } } + + # Set user FUP state + # NK: Perhaps this should be moved to the mod_feature_fup module? + my $fupState = $user->{'AttributeConditionalVariables'}->{"SMRadius_FUP"}; + if (defined($fupState)) { + $fupState = $fupState->[0]; + } else { + $fupState = "-1"; + } + $res = $user->{'_UserDB'}->{'Users_data_set'}($server,$user, + 'mod_feature_fup','State', + $fupState + ); + if (!defined($res)) { + $server->log(LOG_ERR,"[MOD_USERS_DATA] Failed to store FUP state for user '".$user->{'Username'}."'"); + return MOD_RES_SKIP; + } } return MOD_RES_ACK; diff --git a/lib/smradius/modules/system/mod_config_sql_topups.pm b/lib/smradius/modules/system/mod_config_sql_topups.pm index 3309c98ed926da336dc8cc1a0d7817e39c681f61..28f3b3b0b9fb003899138a7618051a6d11a58bad 100644 --- a/lib/smradius/modules/system/mod_config_sql_topups.pm +++ b/lib/smradius/modules/system/mod_config_sql_topups.pm @@ -383,8 +383,8 @@ sub cleanup # Our usage hash my %usageTotals; - $usageTotals{'TotalSessionTime'} = Math::BigInt->new(); - $usageTotals{'TotalDataUsage'} = Math::BigInt->new(); + $usageTotals{'TotalSessionTime'} = Math::BigInt->new(0); + $usageTotals{'TotalDataUsage'} = Math::BigInt->new(0); # Pull in usage and add up if (my $row = hashifyLCtoMC($sth->fetchrow_hashref(), diff --git a/lib/smradius/util.pm b/lib/smradius/util.pm index 2c500ee9aeb0c8f2688acc5dfd39521b9ff5e796..749dd352979b8769848e7fa3e64cf3629e1c7e75 100644 --- a/lib/smradius/util.pm +++ b/lib/smradius/util.pm @@ -85,9 +85,10 @@ sub templateReplace $placeholder //= '?'; # Replace blanks - while (my ($entireMacro,$section,$item,$default) = ($string =~ /(\%{([a-z]+)\.([a-z0-9\-]+)(?:=([^}]*))?})/i )) { - # Replace macro with ? - $string =~ s/$entireMacro/$placeholder/; + while (my ($entireMacro,$section,$item,$default) = ($string =~ /(\%\{([a-z]+)\.([a-z0-9\-]+)(?:=([^\}]*))?\})/i )) { + # Replace macro with ? or the placeholder if specified + # We also quote the entireMacro + $string =~ s/\Q$entireMacro\E/$placeholder/; # Get value to substitute my $value = (defined($hashref->{$section}) && defined($hashref->{$section}->{$item})) ? diff --git a/smradiusd.conf b/smradiusd.conf index 638df3a3686a6315bbbc3192849d99cd31ffb967..b54a5a2f0ae73206361698f53f8173c74d4908aa 100644 --- a/smradiusd.conf +++ b/smradiusd.conf @@ -160,9 +160,10 @@ EOT [features] modules=<run( "--raddb","dicts", "127.0.0.1", @@ -191,7 +200,34 @@ if ($child = fork()) { ); is(ref($res),"HASH","smradclient should return a HASH"); is($res->{'response'}->{'code'},"Access-Accept","Check our return is 'Access-Accept' for bare user blank '' realm"); + # Test the normal attribute and vendor attribute + is($res->{'response'}->{'attributes'}->{'Framed-IP-Address'},"10.0.0.1","Check that attribute 'Framed-IP-Address' is". + " returned"); + is($res->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1024k/512k","Check that the vendor attribute". + "'14988:Mikrotik-Rate-Limit' is returned"); + + # Add filter attributes + my $user1attr4_ID = testDBInsert("Create user 'testuser1' filter attribute for 'Framed-IP-Address'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user1_ID,'SMRadius-Config-Filter-Reply-Attribute',':=','Framed-IP-Address' + ); + my $user1attr5_ID = testDBInsert("Create user 'testuser1' filter vattribute for 'Mikrotik-Rate-Limit'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user1_ID,'SMRadius-Config-Filter-Reply-VAttribute',':=','Mikrotik-Rate-Limit' + ); + $res = smradius::client->run( + "--raddb","dicts", + "127.0.0.1", + "auth", + "secret123", + 'User-Name=testuser1', + 'User-Password=test123', + ); + is(ref($res),"HASH","smradclient should return a HASH"); + # We shouldn't.... + isnt($res->{'response'}->{'attributes'}->{'Framed-IP-Address'},"10.0.0.1","Check that attribute 'Framed-IP-Address' is". + " returned"); # # Modify data for the default realm @@ -464,7 +500,7 @@ if ($child = fork()) { # Test missing accounting START packet # - my $session2_ID = 81700217; + my $session2_ID = "817a0f1b"; my $session2_Timestamp = time(); my $session2_Timestamp_str = DateTime->from_epoch(epoch => $session2_Timestamp,time_zone => 'UTC') ->strftime('%Y-%m-%d %H:%M:%S'); @@ -714,10 +750,8 @@ if ($child = fork()) { ); - my $session3_ID = 9858240; + my $session3_ID = "9c5f24a"; my $session3_Timestamp = time(); - my $session3_Timestamp_str = DateTime->from_epoch(epoch => $session3_Timestamp,time_zone => 'UTC') - ->strftime('%Y-%m-%d %H:%M:%S'); $res = smradius::client->run( "--raddb","dicts", @@ -775,6 +809,210 @@ if ($child = fork()) { ); + # + # Check that if we send an accounting ALIVE we do not trigger FUP + # + + my $user5_ID = testDBInsert("Create user 'testuser5'", + "INSERT INTO users (UserName,Disabled) VALUES ('testuser5',0)" + ); + + my $user5attr1_ID = testDBInsert("Create user 'testuser5' attribute 'User-Password'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user5_ID,'User-Password','==','test456' + ); + + my $user5attr2_ID = testDBInsert("Create user 'testuser5' attribute 'SMRadius-FUP-Period'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user5_ID,'SMRadius-FUP-Period',':=','1' + ); + + my $user5attr3_ID = testDBInsert("Create user 'testuser5' attribute 'SMRadius-FUP-Traffic-Threshold'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user5_ID,'SMRadius-FUP-Traffic-Threshold',':=',800 + ); + + # Add an attribute so we can check the FUP match results + my $user5attr4_ID = testDBInsert("Create user 'testuser5' attribute 'SMRadius-Evaluate'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user5_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m : [14988:Mikrotik-Rate-Limit] = 1k/1m" + ); + + my $session4_ID = "a8abc40"; + my $session4_Timestamp = time(); + + $res = smradius::client->run( + "--raddb","dicts", + "--listen","127.0.0.1:1700", + "127.0.0.1", + "acct", + "secret123", + 'User-Name=testuser5', + 'NAS-IP-Address=10.0.0.1', + 'Acct-Delay-Time=12', + 'NAS-Identifier=Test-NAS2', + 'Acct-Status-Type=Interim-Update', + 'Acct-Output-Packets=786933', + 'Acct-Output-Gigawords=0', + 'Acct-Output-Octets=708163705', + 'Acct-Input-Packets=670235', + 'Acct-Input-Gigawords=0', + 'Acct-Input-Octets=102600046', + 'Acct-Session-Time=800', + 'Event-Timestamp='.$session4_Timestamp, + 'Framed-IP-Address=10.0.1.1', + 'Acct-Session-Id='.$session4_ID, + 'NAS-Port-Id=wlan1', + 'Called-Station-Id=testservice2', + 'Calling-Station-Id=00:00:0C:EE:47:BF', + 'NAS-Port-Type=Ethernet', + 'NAS-Port=15729175', + 'Framed-Protocol=PPP', + 'Service-Type=Framed-User', + ); + is(ref($res),"HASH","smradclient should return a HASH"); + is($res->{'listen'}->{'response'}->{'code'},"CoA-Request","Check that the packet we got back is infact a ". + "CoA-Request"); + is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1k/1m","Check that the vendor attribute". + "'14988:Mikrotik-Rate-Limit' is returned on the negative side of the IF"); + + testDBResults("Check FUP state was added to the user stats table as 0",'users_data', + {'UserID' => $user5_ID, 'Name' => "mod_feature_fup/State"}, + {'Value' => "0"}, + 1, # Disable order + ); + + + # + # Check that if we send an accounting ALIVE with a usage amount that exceeds FUP, that we trigger it + # + + my $user6_ID = testDBInsert("Create user 'testuser6'", + "INSERT INTO users (UserName,Disabled) VALUES ('testuser6',0)" + ); + + my $user6attr1_ID = testDBInsert("Create user 'testuser6' attribute 'User-Password'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user6_ID,'User-Password','==','test456' + ); + + my $user6attr2_ID = testDBInsert("Create user 'testuser6' attribute 'SMRadius-FUP-Period'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user6_ID,'SMRadius-FUP-Period',':=','1' + ); + + my $user6attr3_ID = testDBInsert("Create user 'testuser6' attribute 'SMRadius-FUP-Traffic-Threshold'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user6_ID,'SMRadius-FUP-Traffic-Threshold',':=',800 + ); + + # Add an attribute so we can check the FUP match results + my $user6attr4_ID = testDBInsert("Create user 'testuser6' attribute 'SMRadius-Evaluate'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user6_ID,'SMRadius-Evaluate','||+=',"SMRadius_FUP > 0 ? [14988:Mikrotik-Rate-Limit] = 1638k/8m : [14988:Mikrotik-Rate-Limit] = 1k/1m" + ); + + my $session5_ID = "582dc00"; + my $session5_Timestamp = time(); + + $res = smradius::client->run( + "--raddb","dicts", + "--listen","127.0.0.1:1700", + "127.0.0.1", + "acct", + "secret123", + 'User-Name=testuser6', + 'NAS-IP-Address=10.0.0.1', + 'Acct-Delay-Time=12', + 'NAS-Identifier=Test-NAS2', + 'Acct-Status-Type=Interim-Update', + 'Acct-Output-Packets=786933', + 'Acct-Output-Gigawords=0', + 'Acct-Output-Octets=808163705', + 'Acct-Input-Packets=670235', + 'Acct-Input-Gigawords=0', + 'Acct-Input-Octets=202600046', + 'Acct-Session-Time=800', + 'Event-Timestamp='.$session5_Timestamp, + 'Framed-IP-Address=10.0.1.1', + 'Acct-Session-Id='.$session5_ID, + 'NAS-Port-Id=wlan1', + 'Called-Station-Id=testservice2', + 'Calling-Station-Id=00:00:0C:EE:47:BF', + 'NAS-Port-Type=Ethernet', + 'NAS-Port=15729175', + 'Framed-Protocol=PPP', + 'Service-Type=Framed-User', + ); + is(ref($res),"HASH","smradclient should return a HASH"); + is($res->{'listen'}->{'response'}->{'code'},"CoA-Request","Check that the packet we got back is infact a ". + "CoA-Request"); + is($res->{'listen'}->{'response'}->{'vattributes'}->{'14988'}->{'Mikrotik-Rate-Limit'}->[0],"1638k/8m","Check that the ". + "vendor attribute '14988:Mikrotik-Rate-Limit' is returned on the success side of the FUP check"); + + testDBResults("Check FUP state was added to the user stats table as 1",'users_data', + {'UserID' => $user6_ID, 'Name' => "mod_feature_fup/State"}, + {'Value' => "1"}, + 1, # Disable order + ); + + + # + # Check that if we send an accounting ALIVE with a usage amount that exceeds capping, that we trigger a POD + # + + my $user7_ID = testDBInsert("Create user 'testuser7'", + "INSERT INTO users (UserName,Disabled) VALUES ('testuser7',0)" + ); + + my $user7attr1_ID = testDBInsert("Create user 'testuser7' attribute 'User-Password'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user7_ID,'User-Password','==','test456' + ); + + # Add an attribute so we can check the capping does a POD + my $user7attr4_ID = testDBInsert("Create user 'testuser7' attribute 'SMRadius-Capping-Traffic-Limit'", + "INSERT INTO user_attributes (UserID,Name,Operator,Value,Disabled) VALUES (?,?,?,?,0)", + $user7_ID,'SMRadius-Capping-Traffic-Limit',':=','1' + ); + + + my $session6_ID = "5209dac0"; + my $session6_Timestamp = time(); + + $res = smradius::client->run( + "--raddb","dicts", + "--listen","127.0.0.1:1700", + "127.0.0.1", + "acct", + "secret123", + 'User-Name=testuser7', + 'NAS-IP-Address=10.0.0.1', + 'Acct-Delay-Time=12', + 'NAS-Identifier=Test-NAS2', + 'Acct-Status-Type=Interim-Update', + 'Acct-Output-Packets=786933', + 'Acct-Output-Gigawords=0', + 'Acct-Output-Octets=808163705', + 'Acct-Input-Packets=670235', + 'Acct-Input-Gigawords=0', + 'Acct-Input-Octets=202600046', + 'Acct-Session-Time=800', + 'Event-Timestamp='.$session6_Timestamp, + 'Framed-IP-Address=10.0.1.1', + 'Acct-Session-Id='.$session6_ID, + 'NAS-Port-Id=wlan1', + 'Called-Station-Id=testservice2', + 'Calling-Station-Id=00:00:0C:EE:47:BF', + 'NAS-Port-Type=Ethernet', + 'NAS-Port=15729175', + 'Framed-Protocol=PPP', + 'Service-Type=Framed-User', + ); + is(ref($res),"HASH","smradclient should return a HASH"); + is($res->{'listen'}->{'response'}->{'code'},"Disconnect-Request","Check that the packet we got back is infact a ". + "Disconnect-Request"); + sleep(5); @@ -857,7 +1095,7 @@ sub testDBDelete # Test DB select results sub testDBResults { - my ($name,$table,$where,$resultCheck) = @_; + my ($name,$table,$where,$resultCheck,$disableOrder) = @_; # Build column list @@ -872,22 +1110,29 @@ sub testDBResults # Add data for template placeholders push(@whereData,$where->{$columnName}); } - my $whereLines_str = join(',',@whereLines); + my $whereLines_str = join(' AND ',@whereLines); - # Do select - my $sth = DBSelect(" + # Check if we're not disabling ordering + my $extraSQL = ""; + if (!defined($disableOrder)) { + $extraSQL = "ORDER BY ID DESC"; + } + + my $sqlQuery = " SELECT $columnList_str FROM $table WHERE $whereLines_str - ORDER BY - ID DESC - ",@whereData); + $extraSQL + "; + + # Do select + my $sth = DBSelect($sqlQuery,@whereData); # Make sure we got no error - is(AWITPT::DB::DBLayer::error(),"","Errors on DBSelect: $name"); + is(AWITPT::DB::DBLayer::error(),"","Errors on DBSelect ($sqlQuery interpolated with ".join(', ',$extraSQL).": $name"); # We should get one result... my $row = hashifyLCtoMC($sth->fetchrow_hashref(),keys %{$resultCheck});