Commit d82d2454 authored by Nigel Kukard's avatar Nigel Kukard

Merge branch 'FUP' into 'master'

Added FUP support

See merge request !425
parents 71f628a0 f24d760c
Pipeline #3767 passed with stages
in 4 minutes and 2 seconds
......@@ -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
......
......@@ -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);
......
......@@ -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:
......
# 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,6 +71,7 @@ sub run
\%{$cmdline},
"config:s",
"raddb:s",
"listen:s",
"help",
)) {
print(STDERR "ERROR: Error parsing commandline arguments");
......@@ -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;
......
......@@ -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,
};
......
This diff is collapsed.
# 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) = @_;
......
# 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
......@@ -147,6 +147,23 @@ sub updateUserStats
return MOD_RES_SKIP;
}
}