mod_feature_capping.pm 24.7 KB
Newer Older
1
# Capping support
2 3
# Copyright (C) 2007-2016, AllWorldIT
#
4 5 6 7
# 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.
8
#
9 10 11 12
# 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.
13
#
14 15 16 17
# 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.

18
package smradius::modules::features::mod_feature_capping;
19 20 21 22 23

use strict;
use warnings;

# Modules we need
24
use smradius::attributes;
25 26 27 28
use smradius::constants;
use smradius::logging;
use smradius::util;

29
use AWITPT::Util;
30 31 32
use List::Util qw( min );
use MIME::Lite;
use POSIX qw( floor );
33 34


35 36
# Load exporter
use base qw(Exporter);
Nigel Kukard's avatar
Nigel Kukard committed
37 38
our @EXPORT = qw(
);
39
our @EXPORT_OK = qw(
40 41 42 43 44 45 46 47
);



# Plugin info
our $pluginInfo = {
	Name => "User Capping Feature",
	Init => \&init,
Robert Anderson's avatar
Robert Anderson committed
48

49 50 51 52 53 54 55 56 57
	# Authentication hook
	'Feature_Post-Authentication_hook' => \&post_auth_hook,

	# Accounting hook
	'Feature_Post-Accounting_hook' => \&post_acct_hook,
};


# Some constants
58 59 60 61 62 63
my $TRAFFIC_LIMIT_ATTRIBUTE = 'SMRadius-Capping-Traffic-Limit';
my $UPTIME_LIMIT_ATTRIBUTE = 'SMRadius-Capping-Uptime-Limit';

my $TRAFFIC_TOPUP_ATTRIBUTE = 'SMRadius-Capping-Traffic-Topup';
my $TIME_TOPUP_ATTRIBUTE = 'SMRadius-Capping-Uptime-Topup';

64 65
my $config;

66 67


68 69 70 71 72
## @internal
# Initialize module
sub init
{
	my $server = shift;
73 74 75 76 77
	my $scfg = $server->{'inifile'};


	# Setup SQL queries
	if (defined($scfg->{'mod_feature_capping'})) {
78 79 80 81 82 83 84 85 86 87 88 89
		# Check if option exists
		if (defined($scfg->{'mod_feature_capping'}{'enable_mikrotik'})) {
			# Pull in config
			if ($scfg->{'mod_feature_capping'}{'enable_mikrotik'} =~ /^\s*(yes|true|1)\s*$/i) {
				$server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] Mikrotik-specific vendor return attributes ENABLED");
				$config->{'enable_mikrotik'} = $scfg->{'mod_feature_capping'}{'enable_mikrotik'};
			# Default?
			} elsif ($scfg->{'mod_feature_capping'}{'enable_mikrotik'} =~ /^\s*(no|false|0)\s*$/i) {
				$config->{'enable_mikrotik'} = undef;
			} else {
				$server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] Value for 'enable_mikrotik' is invalid");
			}
90 91 92
		}
	}

93
	return;
94 95 96
}


97

98 99 100 101 102 103 104 105 106 107 108 109
## @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) = @_;

Robert Anderson's avatar
Robert Anderson committed
110

111 112 113 114
	# Skip MAC authentication
	return MOD_RES_SKIP if ($user->{'_UserDB'}->{'Name'} eq "SQL User Database (MAC authentication)");

	$server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] POST AUTH HOOK");
Robert Anderson's avatar
Robert Anderson committed
115

116

Robert Anderson's avatar
Robert Anderson committed
117 118 119 120
	#
	# Get limits from attributes
	#

121 122
	my $uptimeLimit = _getAttributeKeyLimit($server,$user,$UPTIME_LIMIT_ATTRIBUTE);
	my $trafficLimit = _getAttributeKeyLimit($server,$user,$TRAFFIC_LIMIT_ATTRIBUTE);
Robert Anderson's avatar
Robert Anderson committed
123

124

Robert Anderson's avatar
Robert Anderson committed
125 126 127 128
	#
	# Get current traffic and uptime usage
	#

129 130 131
	my $accountingUsage = _getAccountingUsage($server,$user,$packet);
	if (!defined($accountingUsage)) {
		return MOD_RES_SKIP;
132 133
	}

134

Robert Anderson's avatar
Robert Anderson committed
135 136 137 138
	#
	# Get valid traffic and uptime topups
	#

139 140 141
	# Check if there was any data returned at all
	my $uptimeTopupAmount = _getConfigAttributeNumeric($server,$user,$TIME_TOPUP_ATTRIBUTE) // 0;
	my $trafficTopupAmount = _getConfigAttributeNumeric($server,$user,$TRAFFIC_TOPUP_ATTRIBUTE) // 0;
Robert Anderson's avatar
Robert Anderson committed
142

143

Robert Anderson's avatar
Robert Anderson committed
144 145 146 147 148
	#
	# Set the new uptime and traffic limits (limit, if any.. + topups)
	#

	# Uptime..
149 150
	# // is a defined operator,  $a ? defined($a) : $b
	my $uptimeLimitWithTopups = ($uptimeLimit // 0) + $uptimeTopupAmount;
Robert Anderson's avatar
Robert Anderson committed
151 152

	# Traffic..
153 154
	# // is a defined operator,  $a ? defined($a) : $b
	my $trafficLimitWithTopups = ($trafficLimit // 0) + $trafficTopupAmount;
Robert Anderson's avatar
Robert Anderson committed
155

Robert Anderson's avatar
Robert Anderson committed
156

157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
	#
	# Do auto-topups for both traffic and uptime
	#

	my $autoTopupTrafficAdded = _doAutoTopup($server,$user,$accountingUsage->{'TotalDataUsage'},"traffic",
			$trafficLimitWithTopups,1);
	if (defined($autoTopupTrafficAdded)) {
		$trafficLimitWithTopups += $autoTopupTrafficAdded;
	}

	my $autoTopupUptimeAdded = _doAutoTopup($server,$user,$accountingUsage->{'TotalSessionTime'},"uptime",
			$uptimeLimitWithTopups,2);
	if (defined($autoTopupUptimeAdded)) {
		$uptimeLimitWithTopups += $autoTopupUptimeAdded;
	}


Robert Anderson's avatar
Robert Anderson committed
174 175 176
	#
	# Display our usages
	#
177 178 179

	_logUsage($server,$accountingUsage->{'TotalDataUsage'},$uptimeLimit,$uptimeTopupAmount,'traffic');
	_logUsage($server,$accountingUsage->{'TotalSessionTime'},$uptimeLimit,$uptimeTopupAmount,'uptime');
Robert Anderson's avatar
Robert Anderson committed
180 181


182 183 184
	#
	# Add conditional variables
	#
Robert Anderson's avatar
Robert Anderson committed
185

186 187 188
	addAttributeConditionalVariable($user,"SMRadius_Capping_TotalDataUsage",$accountingUsage->{'TotalDataUsage'});
	addAttributeConditionalVariable($user,"SMRadius_Capping_TotalSessionTime",$accountingUsage->{'TotalSessionTime'});

189

190 191 192 193 194 195
	#
	# Allow for capping overrides by client attribute
	#

	if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'})) {
		my $multiplier = pop(@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'}});
196

Nigel Kukard's avatar
Nigel Kukard committed
197
		my $newLimit = $uptimeLimitWithTopups * $multiplier;
198 199
		my $newSessionTime = $accountingUsage->{'TotalSessionTime'} * $multiplier;

Nigel Kukard's avatar
Nigel Kukard committed
200
		$uptimeLimitWithTopups = $newLimit;
201 202 203
		$accountingUsage->{'TotalSessionTime'} = $newSessionTime;

		$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client uptime multiplier '$multiplier' changes ".
Nigel Kukard's avatar
Nigel Kukard committed
204
				"uptime limit ('$uptimeLimitWithTopups' => '$newLimit'), ".
205 206
				"uptime usage ('".$accountingUsage->{'TotalSessionTime'}."' => '$newSessionTime')"
		);
207 208 209
	}
	if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Traffic-Multiplier'})) {
		my $multiplier = pop(@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Traffic-Multiplier'}});
210

Nigel Kukard's avatar
Nigel Kukard committed
211
		my $newLimit = $trafficLimitWithTopups * $multiplier;
212 213
		my $newDataUsage = $accountingUsage->{'TotalDataUsage'} * $multiplier;

Nigel Kukard's avatar
Nigel Kukard committed
214
		$trafficLimitWithTopups = $newLimit;
215
		$accountingUsage->{'TotalDataUsage'} = $newDataUsage;
216 217

		$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client traffic multiplier '$multiplier' changes ".
Nigel Kukard's avatar
Nigel Kukard committed
218
				"traffic limit ('$trafficLimitWithTopups' => '$newLimit'), ".
219 220
				"traffic usage ('".$accountingUsage->{'TotalDataUsage'}."' => '$newDataUsage')"
		);
221 222
	}

223

Robert Anderson's avatar
Robert Anderson committed
224 225 226 227 228
	#
	# Check if we've exceeded our limits
	#

	# Uptime..
229
	if (!defined($uptimeLimit) || $uptimeLimit > 0) {
Robert Anderson's avatar
Robert Anderson committed
230

231
		# Check session time has not exceeded what we're allowed
Nigel Kukard's avatar
Nigel Kukard committed
232
		if ($accountingUsage->{'TotalSessionTime'} >= $uptimeLimitWithTopups) {
233
			$server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] Usage of ".$accountingUsage->{'TotalSessionTime'}.
234
					"min exceeds allowed limit of ".$uptimeLimitWithTopups."min");
235
			return MOD_RES_NACK;
236 237 238
		# Setup limits
		} else {
			# Check if we returning Mikrotik vattributes
Nigel Kukard's avatar
Nigel Kukard committed
239
			# FIXME: NK - this is not mikrotik specific
240
			if (defined($config->{'enable_mikrotik'})) {
Nigel Kukard's avatar
Nigel Kukard committed
241
				# FIXME: NK - We should cap the maximum total session time to that which is already set, if something is set
242 243 244 245
				# Setup reply attributes for Mikrotik HotSpots
				my %attribute = (
					'Name' => 'Session-Timeout',
					'Operator' => '=',
Nigel Kukard's avatar
Nigel Kukard committed
246
					'Value' => $uptimeLimitWithTopups - $accountingUsage->{'TotalSessionTime'}
247 248 249
				);
				setReplyAttribute($server,$user->{'ReplyAttributes'},\%attribute);
			}
250 251 252
		}
	}

Robert Anderson's avatar
Robert Anderson committed
253
	# Traffic
254
	if (!defined($trafficLimit) || $trafficLimit > 0) {
Robert Anderson's avatar
Robert Anderson committed
255 256

		# Capped
Nigel Kukard's avatar
Nigel Kukard committed
257
		if ($accountingUsage->{'TotalDataUsage'} >= $trafficLimitWithTopups) {
Robert Anderson's avatar
Robert Anderson committed
258
			$server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] Usage of ".$accountingUsage->{'TotalDataUsage'}.
259
					"Mbyte exceeds allowed limit of ".$trafficLimitWithTopups."Mbyte");
Robert Anderson's avatar
Robert Anderson committed
260
			return MOD_RES_NACK;
261 262 263 264 265
		# Setup limits
		} else {
			# Check if we returning Mikrotik vattributes
			if (defined($config->{'enable_mikrotik'})) {
				# Get remaining traffic
Nigel Kukard's avatar
Nigel Kukard committed
266
				my $remainingTraffic = $trafficLimitWithTopups - $accountingUsage->{'TotalDataUsage'};
267
				my $remainingTrafficLimit = ( $remainingTraffic % 4096 ) * 1024 * 1024;
268
				my $remainingTrafficGigawords = floor($remainingTraffic / 4096);
269

270
				# Setup reply attributes for Mikrotik HotSpots
Nigel Kukard's avatar
Nigel Kukard committed
271
				foreach my $attrName ('Recv','Xmit','Total') {
272 273 274 275 276 277 278 279
					my %attribute = (
						'Vendor' => 14988,
						'Name' => "Mikrotik-$attrName-Limit",
						'Operator' => '=',
						# Gigawords leftovers
						'Value' => $remainingTrafficLimit
					);
					setReplyVAttribute($server,$user->{'ReplyVAttributes'},\%attribute);
280

281 282 283 284 285 286 287 288 289 290
					%attribute = (
						'Vendor' => 14988,
						'Name' => "Mikrotik-$attrName-Limit-Gigawords",
						'Operator' => '=',
						# Gigawords
						'Value' => $remainingTrafficGigawords
					);
					setReplyVAttribute($server,$user->{'ReplyVAttributes'},\%attribute);
				}
			}
Robert Anderson's avatar
Robert Anderson committed
291 292 293
		}
	}

294 295 296 297
	return MOD_RES_ACK;
}


298

299 300 301 302 303 304 305 306 307 308 309 310
## @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) = @_;

Robert Anderson's avatar
Robert Anderson committed
311

312 313 314 315 316 317
	# 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)");

Robert Anderson's avatar
Robert Anderson committed
318
	# Exceeding maximum, must be disconnected
319
	return MOD_RES_SKIP if ($packet->rawattr('Acct-Status-Type') ne "1" && $packet->rawattr('Acct-Status-Type') ne "3");
Robert Anderson's avatar
Robert Anderson committed
320

321 322
	$server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] POST ACCT HOOK");

Robert Anderson's avatar
Robert Anderson committed
323 324 325 326 327

	#
	# Get limits from attributes
	#

328 329
	my $uptimeLimit = _getAttributeKeyLimit($server,$user,$UPTIME_LIMIT_ATTRIBUTE);
	my $trafficLimit = _getAttributeKeyLimit($server,$user,$TRAFFIC_LIMIT_ATTRIBUTE);
Robert Anderson's avatar
Robert Anderson committed
330

331

Robert Anderson's avatar
Robert Anderson committed
332 333 334
	#
	# Get current traffic and uptime usage
	#
335 336 337 338
	#
	my $accountingUsage = _getAccountingUsage($server,$user,$packet);
	if (!defined($accountingUsage)) {
		return MOD_RES_SKIP;
339
	}
340

Robert Anderson's avatar
Robert Anderson committed
341 342 343 344 345 346

	#
	# Get valid traffic and uptime topups
	#

	# Check if there was any data returned at all
347 348
	my $uptimeTopupAmount = _getConfigAttributeNumeric($server,$user,$TIME_TOPUP_ATTRIBUTE) // 0;
	my $trafficTopupAmount = _getConfigAttributeNumeric($server,$user,$TRAFFIC_TOPUP_ATTRIBUTE) // 0;
349

Robert Anderson's avatar
Robert Anderson committed
350

Robert Anderson's avatar
Robert Anderson committed
351 352 353 354 355
	#
	# Set the new uptime and traffic limits (limit, if any.. + topups)
	#

	# Uptime..
356 357
	# // is a defined operator,  $a ? defined($a) : $b
	my $uptimeLimitWithTopups = ($uptimeLimit // 0) + $uptimeTopupAmount;
Robert Anderson's avatar
Robert Anderson committed
358 359

	# Traffic..
360 361
	# // is a defined operator,  $a ? defined($a) : $b
	my $trafficLimitWithTopups = ($trafficLimit // 0) + $trafficTopupAmount;
Robert Anderson's avatar
Robert Anderson committed
362

363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380

	#
	# Do auto-topups for both traffic and uptime
	#

	my $autoTopupTrafficAdded = _doAutoTopup($server,$user,$accountingUsage->{'TotalDataUsage'},"traffic",
			$trafficLimitWithTopups,1);
	if (defined($autoTopupTrafficAdded)) {
		$trafficLimitWithTopups += $autoTopupTrafficAdded;
	}

	my $autoTopupUptimeAdded = _doAutoTopup($server,$user,$accountingUsage->{'TotalSessionTime'},"uptime",
			$uptimeLimitWithTopups,2);
	if (defined($autoTopupUptimeAdded)) {
		$uptimeLimitWithTopups += $autoTopupUptimeAdded;
	}


Robert Anderson's avatar
Robert Anderson committed
381 382 383 384
	#
	# Display our usages
	#

385 386
	_logUsage($server,$accountingUsage->{'TotalDataUsage'},$uptimeLimit,$uptimeTopupAmount,'traffic');
	_logUsage($server,$accountingUsage->{'TotalSessionTime'},$uptimeLimit,$uptimeTopupAmount,'uptime');
Robert Anderson's avatar
Robert Anderson committed
387

Robert Anderson's avatar
Robert Anderson committed
388

389 390 391
	#
	# Add conditional variables
	#
392

393 394 395 396
	# Add attribute conditionals BEFORE override
	addAttributeConditionalVariable($user,"SMRadius_Capping_TotalDataUsage",$accountingUsage->{'TotalDataUsage'});
	addAttributeConditionalVariable($user,"SMRadius_Capping_TotalSessionTime",$accountingUsage->{'TotalSessionTime'});

397

398
	#
399
	# Allow for capping overrides by client attribute
400 401
	#

402 403
	if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'})) {
		my $multiplier = pop(@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Uptime-Multiplier'}});
Nigel Kukard's avatar
Nigel Kukard committed
404
		my $newLimit = $uptimeLimitWithTopups * $multiplier;
405
		$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client cap uptime multiplier '$multiplier' changes limit ".
Nigel Kukard's avatar
Nigel Kukard committed
406 407
				"from '$uptimeLimitWithTopups' to '$newLimit'");
		$uptimeLimitWithTopups = $newLimit;
408 409 410
	}
	if (defined($user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Traffic-Multiplier'})) {
		my $multiplier = pop(@{$user->{'ConfigAttributes'}->{'SMRadius-Config-Capping-Traffic-Multiplier'}});
Nigel Kukard's avatar
Nigel Kukard committed
411
		my $newLimit = $trafficLimitWithTopups * $multiplier;
412
		$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Client cap traffic multiplier '$multiplier' changes limit ".
Nigel Kukard's avatar
Nigel Kukard committed
413 414
				"from '$trafficLimitWithTopups' to '$newLimit'");
		$trafficLimitWithTopups = $newLimit;
415 416 417
	}


Robert Anderson's avatar
Robert Anderson committed
418 419 420 421 422
	#
	# Check if we've exceeded our limits
	#

	# Uptime..
423
	if (!defined($uptimeLimit) || $uptimeLimit > 0) {
Robert Anderson's avatar
Robert Anderson committed
424 425

		# Capped
Nigel Kukard's avatar
Nigel Kukard committed
426
		if ($accountingUsage->{'TotalSessionTime'} >= $uptimeLimitWithTopups) {
427
			$server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] Usage of ".$accountingUsage->{'TotalSessionTime'}.
428
					"min exceeds allowed limit of ".$uptimeLimitWithTopups."min");
429 430 431 432
			return MOD_RES_NACK;
		}
	}

Robert Anderson's avatar
Robert Anderson committed
433
	# Traffic
434
	if (!defined($trafficLimit) || $trafficLimit > 0) {
Robert Anderson's avatar
Robert Anderson committed
435 436

		# Capped
Nigel Kukard's avatar
Nigel Kukard committed
437
		if ($accountingUsage->{'TotalDataUsage'} >= $trafficLimitWithTopups) {
Robert Anderson's avatar
Robert Anderson committed
438
			$server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] Usage of ".$accountingUsage->{'TotalDataUsage'}.
439
					"Mbyte exceeds allowed limit of ".$trafficLimitWithTopups."Mbyte");
Robert Anderson's avatar
Robert Anderson committed
440 441 442 443
			return MOD_RES_NACK;
		}
	}

444 445
	return MOD_RES_ACK;
}
446

Nigel Kukard's avatar
Nigel Kukard committed
447 448


449 450 451 452 453 454 455 456
## @internal
# Code snippet to grab the current uptime limit by processing the user attributes
sub _getAttributeKeyLimit
{
	my ($server,$user,$attributeKey) = @_;


	# Short circuit return if we don't have the uptime key set
457
	return if (!defined($user->{'Attributes'}->{$attributeKey}));
458 459

	# Short circuit if we do not have a valid attribute operator: ':='
460
	if (!defined($user->{'Attributes'}->{$attributeKey}->{':='})) {
461
		$server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] No valid operators for attribute '".
462
				$user->{'Attributes'}->{$attributeKey}."'");
Nigel Kukard's avatar
Nigel Kukard committed
463
		return;
464 465
	}

466
	$server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] Attribute '".$attributeKey."' is defined");
467 468

	# Check for valid attribute value
469 470 471
	if (!defined($user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'}) ||
			$user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'} !~ /^\d+$/) {
		$server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] Attribute '".$user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'}.
472
				"' is NOT a numeric value");
Nigel Kukard's avatar
Nigel Kukard committed
473
		return;
474 475
	}

476
	return $user->{'Attributes'}->{$attributeKey}->{':='}->{'Value'};
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
}



## @internal
# Code snippet to grab the current accounting usage of a user
sub _getAccountingUsage
{
	my ($server,$user,$packet) = @_;


	foreach my $module (@{$server->{'module_list'}}) {
		# Do we have the correct plugin?
		if (defined($module->{'Accounting_getUsage'})) {
			$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Found plugin: '".$module->{'Name'}."'");
			# Fetch users session uptime & bandwidth used
			if (my $res = $module->{'Accounting_getUsage'}($server,$user,$packet)) {
				return $res;
			}
			$server->log(LOG_ERR,"[MOD_FEATURE_CAPPING] No usage data found for user '".$user->{'Username'}."'");
		}
	}

Nigel Kukard's avatar
Nigel Kukard committed
500
	return;
501 502 503 504
}



505
## @internal
Nigel Kukard's avatar
Nigel Kukard committed
506
# Code snippet to log our uptime usage
507
sub _logUsage
508
{
509
	my ($server,$accountingUsage,$limit,$topupAmount,$type) = @_;
510 511


512
	my $typeKey = ucfirst($type);
513 514

	# Check if our limit is defined
515 516
	if (defined($limit) && !$limit) {
		$limit = '-none-';
Nigel Kukard's avatar
Nigel Kukard committed
517
	} else {
518
		$limit = '-topup-';
Nigel Kukard's avatar
Nigel Kukard committed
519 520
	}

521 522 523
	$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Capping information [type: %s, total: %s, limit: %s, topups: %s]",
			$type,$accountingUsage,$limit,$topupAmount);

Nigel Kukard's avatar
Nigel Kukard committed
524 525 526 527 528 529
	return;
}



## @internal
530
# Function snippet to return a user attribute
531
sub _getConfigAttributeNumeric
Nigel Kukard's avatar
Nigel Kukard committed
532
{
533
	my ($server,$user,$attributeName) = @_;
Nigel Kukard's avatar
Nigel Kukard committed
534 535 536


	# Short circuit if the attribute does not exist
Nigel Kukard's avatar
Nigel Kukard committed
537
	return 0 if (!defined($user->{'ConfigAttributes'}->{$attributeName}));
Nigel Kukard's avatar
Nigel Kukard committed
538

Nigel Kukard's avatar
Nigel Kukard committed
539
	$server->log(LOG_DEBUG,"[MOD_FEATURE_CAPPING] Config attribute '".$attributeName."' is defined");
Nigel Kukard's avatar
Nigel Kukard committed
540
	# Check for value
Nigel Kukard's avatar
Nigel Kukard committed
541 542
	if (!defined($user->{'ConfigAttributes'}->{$attributeName}->[0])) {
		$server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] Config attribute '".$attributeName."' has no value");
Nigel Kukard's avatar
Nigel Kukard committed
543 544 545 546
		return 0;
	}

	# Is it a number?
Nigel Kukard's avatar
Nigel Kukard committed
547 548
	if ($user->{'ConfigAttributes'}->{$attributeName}->[0] !~ /^\d+$/) {
		$server->log(LOG_NOTICE,"[MOD_FEATURE_CAPPING] Config attribute '".$user->{'ConfigAttributes'}->{$attributeName}->[0].
Nigel Kukard's avatar
Nigel Kukard committed
549 550
				"' is NOT a numeric value");
		return 0;
551 552
	}

Nigel Kukard's avatar
Nigel Kukard committed
553
	return $user->{'ConfigAttributes'}->{$attributeName}->[0];
554 555 556 557
}



558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807
## @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'};
}



## @internal
# Function which impelments our auto-topup functionality
sub _doAutoTopup
{
	my ($server,$user,$accountingUsage,$type,$usageLimit,$topupType) = @_;
	my $scfg = $server->{'inifile'};


	# Get the key, which has the first letter uppercased
	my $typeKey = ucfirst($type);

	# Booleanize the attribute and check if its enabled
	if (my $enabled = booleanize(_getAttribute($server,$user,"SMRadius-AutoTopup-$typeKey-Enabled"))) {
		$server->log(LOG_INFO,'[MOD_FEATURE_CAPPING] AutoToups for %s is enabled',$type);
	} else {
		$server->log(LOG_DEBUG,'[MOD_FEATURE_CAPPING] AutoToups for %s is not enabled',$type);
		return;
	}

	# Do sanity checks on the auto-topup amount
	my $autoTopupAmount = _getAttribute($server,$user,"SMRadius-AutoTopup-$typeKey-Amount");
	if (!defined($autoTopupAmount)) {
		$server->log(LOG_WARN,'[MOD_FEATURE_CAPPING] SMRadius-AutoToup-%s-Amount must have a value',$typeKey);
		return;
	}
	if (!isNumber($autoTopupAmount)){
		$server->log(LOG_WARN,'[MOD_FEATURE_CAPPING] SMRadius-AutoToup-%s-Amount must be a number and be > 0, instead it was '.
				'\'%s\', IGNORING SMRadius-AutoTopup-%s-Enabled',$typeKey,$autoTopupAmount,$typeKey);
		return;
	}

	# Do sanity checks on the auto-topup threshold
	my $autoTopupThreshold = _getAttribute($server,$user,"SMRadius-AutoTopup-$typeKey-Threshold");
	if (defined($autoTopupThreshold) && !isNumber($autoTopupThreshold)){
		$server->log(LOG_WARN,'[MOD_FEATURE_CAPPING] SMRadius-AutoToup-%s-Threshold must be a number and be > 0, instead it was '.
				'\'%s\', IGNORING SMRadius-AutoTopup-%s-Threshold',$typeKey,$autoTopupAmount,$typeKey);
		$autoTopupThreshold = undef;
	}

	# Check that if the auto-topup limit is defined, that it is > 0
	my $autoTopupLimit = _getAttribute($server,$user,"SMRadius-AutoTopup-$typeKey-Limit");
	if (defined($autoTopupLimit) && !isNumber($autoTopupLimit)) {
		$server->log(LOG_WARN,'[MOD_FEATURE_CAPPING] SMRadius-AutoToup-%s-Limit must be a number and be > 0, instead it was '.
				'\'%s\', IGNORING SMRadius-AutoTopup-%s-Enabled',$typeKey,$autoTopupAmount,$typeKey);
		return;
	}

	# Pull in ahow many auto-topups were already added
	my $autoTopupsAdded = _getConfigAttributeNumeric($server,$user,"SMRadius-Capping-$typeKey-AutoTopup") // 0;

	# Default to an auto-topup threshold of the topup amount divided by two if none has been provided
	$autoTopupThreshold //= floor($autoTopupAmount / 2);

	# Check if we're still within our usage limit
	return if ($accountingUsage + $autoTopupThreshold < $usageLimit + $autoTopupsAdded);

	# Check the difference between our accounting usage and our usage limit
	my $usageDelta = $accountingUsage - $usageLimit;
	# Make sure our delta is at least 0
	$usageDelta = 0 if ($usageDelta < 0);

	# Calculate how many topups are needed
	my $autoTopupsRequired = floor($usageDelta / $autoTopupAmount) + 1;

	# Default the topups to add to the number required
	my $autoTopupsToAdd = $autoTopupsRequired;

	# If we have an auto-topup limit, recalculate how many we must add... maybe it exceeds
	if (defined($autoTopupLimit)) {
		my $autoTopupsAllowed = floor(($autoTopupLimit - $autoTopupsAdded) / $autoTopupAmount);
		$autoTopupsToAdd = min($autoTopupsRequired,$autoTopupsAllowed);
		# We cannot add a negative amount of auto-topups, if we have a negative amount, we have hit our limit
		$autoTopupsToAdd = 0 if ($autoTopupsToAdd < 0);
	}

	# Total topup amount
	my $autoTopupsToAddAmount = $autoTopupsToAdd * $autoTopupAmount;

	# The datetime now
	my $now = DateTime->now->set_time_zone($server->{'smradius'}->{'event_timezone'});
	# Use truncate to set all values after 'month' to their default values
	my $thisMonth = $now->clone()->truncate( to => "month" );
	# This month, in string form
	my $thisMonth_str = $thisMonth->strftime("%Y-%m-%d");
	# Next month..
	my $nextMonth = $thisMonth->clone()->add( months => 1 );
	my $nextMonth_str = $nextMonth->strftime("%Y-%m-%d");

	# Lets see if a module accepts to add a topup
	my $res;
	foreach my $module (@{$server->{'module_list'}}) {
		# Do we have the correct plugin?
		if (defined($module->{'Feature_Config_Topop_add'})) {
			$server->log(LOG_INFO,"[MOD_FEATURE_CAPPING] Found plugin: '".$module->{'Name'}."'");
			# Try add topup
			$res = $module->{'Feature_Config_Topop_add'}($server,$user,$thisMonth_str,$nextMonth_str,
					($topupType | 4),$autoTopupAmount);
			# Skip to end if we added a topup
			if ($res == MOD_RES_ACK) {
				my $topupsRemaining = $autoTopupsToAdd - 1;
				while ($topupsRemaining > 0) {
					# Try add another topup
					$res = $module->{'Feature_Config_Topop_add'}($server,$user,$thisMonth_str,$nextMonth_str,
							($topupType | 4),$autoTopupAmount);
					$topupsRemaining--;
				}
				last;
			}
		}
	}
	# If not, return undef
	if (!defined($res) || $res != MOD_RES_ACK) {
		$server->log(LOG_WARN,'[MOD_FEATURE_CAPPING] Auto-Topup(s) cannot be added, no module replied with ACK');
		return;
	}

	$server->log(LOG_INFO,'[MOD_FEATURE_CAPPING] Auto-Topups added [type: %s, threshold: %s, amount: %s, required: %s, limit: %s, added: %s]',
			$type,$autoTopupThreshold,$autoTopupAmount,$autoTopupsRequired,$autoTopupLimit,$autoTopupsToAdd);

	# Grab notify destinations
	my $notify;
	if (!defined($notify = _getAttribute($server,$user,"SMRadius-AutoTopup-$typeKey-Notify"))) {
		$server->log(LOG_INFO,'[MOD_FEATURE_CAPPING] AutoToups notify destination is not specified, NOT notifying');
		goto END;
	}
	$server->log(LOG_INFO,'[MOD_FEATURE_CAPPING] AutoToups notify destination is \'%s\'',$notify);

	# Grab notify template
	my $notifyTemplate;
	if (!defined($notifyTemplate = _getAttribute($server,$user,"SMRadius-AutoTopup-$typeKey-NotifyTemplate"))) {
		$server->log(LOG_INFO,'[MOD_FEATURE_CAPPING] AutoToups notify template is not specified, NOT notifying');
		goto END;
	}

	# NOTE: $autoTopupToAdd and autoTopupsToAddAmount will be 0 if no auto-topups were added

	# Create variable hash to pass to TT
	my $variables = {
		'user' => {
			'ID' => $user->{'ID'},
			'username' => $user->{'Username'},
		},
		'usage' => {
			'total' => $accountingUsage,
			'limit' => $usageLimit,
		},
		'autotopup' => {
			'amount' => $autoTopupAmount,
			'limit' => $autoTopupLimit,
			'added' => $autoTopupsAdded,
			'toAdd' => $autoTopupsToAdd,
			'toAddAmount' => $autoTopupsToAddAmount,
		},
	};

	# Split off notification targets
	my @notificationTargets = split(/[,;\s]+/,$notify);

	foreach my $notifyTarget (@notificationTargets) {
		# Parse template
		my ($notifyMsg,$error) = quickTemplateToolkit($notifyTemplate,{
				%{$variables},
				'notify' => { 'target' => $notifyTarget }
		});

		# Check if we have a result, if not, report the error
		if (!defined($notifyMsg)) {
			my $errorMsg = $error->info();
			$errorMsg =~ s/\r?\n/\\n/g;
			$server->log(LOG_WARN,'[MOD_FEATURE_CAPPING] AutoToups notify template parsing failed: %s',$errorMsg);
			next;
		}

		my %messageHeaders = ();

		# Split message into lines
		my @lines = split(/\r?\n/,$notifyMsg);
		while (defined($lines[0]) && (my $line = $lines[0]) =~ /(\S+): (.*)/) {
			my ($header,$value) = ($1,$2);
			$messageHeaders{$header} = $value;
			# Remove line
			shift(@lines);
			# Last if our next line is undefined
			last if (!defined($lines[0]));
			# If the next line is blank, remove it, and continue below
			if ($lines[0] =~ /^\s*$/) {
				# Remove blank line
				shift(@lines);
				last;
			}
		}

		# Create message
		my $msg = MIME::Lite->new(
			'Type' => 'multipart/mixed',
			'Date' => $now->strftime('%a, %d %b %Y %H:%M:%S %z'),
			%messageHeaders
		);

		# Attach body
		$msg->attach(
			'Type' => 'TEXT',
			'Encoding' => '8bit',
			'Data' => join("\n",@lines),
		);

		# Send email
		my $smtpServer = $scfg->{'server'}{'smtp_server'} // 'localhost';
		eval { $msg->send("smtp",$smtpServer); };
		if (my $error = $@) {
			$server->log(LOG_WARN,"[MOD_FEATURE_CAPPING] Email sending failed: '%s'",$error);
		}

	}

END:
	return $autoTopupsToAddAmount;
}



808
1;
Nigel Kukard's avatar
Nigel Kukard committed
809
# vim: ts=4