3 # Copyright (C) 2008 Sky Network Services. All Rights Reserved.
5 # This program is free software; you can redistribute it and/or modify it
6 # under the same terms as Perl itself.
14 use Net::Radius::Packet;
15 use Net::Radius::Dictionary;
16 use NetSNMP::agent qw/:all/;
17 use NetSNMP::ASN qw/:all/;
20 use Log::Log4perl qw/:easy/;
22 #$Data::Dumper::Indent = 1;
23 #$Data::Dumper::Sortkeys = 1;
26 # config. should be really loaded from some file
30 Name => 'freeradius-snmp',
33 oid_root => '1.3.6.1.2.1.67',
35 1 => [qw/auth proxyauth/],
36 2 => [qw/acct proxyacct/],
43 secret => 'adminsecret',
44 # dictionary => '../radiusd/share/dictionary',
45 dictionary => 'dictionary.hacked',
52 layout => '%d{ISO8601} <%p> (%L) %m%n',
58 Log::Log4perl->easy_init($cfg->{log});
61 my %snmp_data :shared;
62 my @snmp_data_k :shared;
65 INFO 'initializing snmp';
66 my $agent = new NetSNMP::agent(%{$cfg->{snmp}->{agent}});
68 #lets create the thread as early as possible (but it has to be AFTER initializing snmp)
69 INFO 'launching radius client thread';
70 threads->create(\&radius_updater);
72 #we export only subtrees, not the whole tree
74 $cfg->{snmp}->{agent}->{Name},
75 $cfg->{snmp}->{oid_root}.'.'.$_, \&snmp_handler) or die
76 foreach keys %{$cfg->{snmp}->{oid_sub}};
78 INFO 'entering client main loop';
81 WARN 'something caused me to exit';
85 # initialize common radius client stuff
86 sub radius_stats_init {
89 $d = new Net::Radius::Dictionary;
90 $d->readfile($cfg->{radius}->{dictionary});
94 $s = new IO::Socket::INET(
95 PeerHost => $cfg->{radius}->{host},
96 PeerPort => $cfg->{radius}->{port},
102 # build server status packet, send it, fetch and parse the result
103 sub radius_stats_get {
104 my ( $type, %args ) = @_;
106 our ( $d, $s, $rid );
108 my $p_req = new Net::Radius::Packet $d;
109 $p_req->set_code('Status-Server');
110 $p_req->set_vsattr('FreeRADIUS', 'FreeRADIUS-Statistics-Type', $type);
111 $p_req->set_vsattr('FreeRADIUS', $_, $args{$_}) foreach keys %args;
114 $p_req->set_identifier($rid++);
115 $p_req->set_authenticator(pack 'C*', map { int rand 255 } 0..15);
117 #recalc authenticator
118 $p_req->set_attr('Message-Authenticator', "\0"x16, 1);
119 $p_req->set_attr('Message-Authenticator', Digest::HMAC_MD5::hmac_md5($p_req->pack, $cfg->{radius}->{secret}), 1);
121 #send brand new and shiny request
122 $s->send($p_req->pack) or die;
125 if ( defined $s->recv($p_data, 2048) ) {
126 my $p_res = new Net::Radius::Packet $d, $p_data;
129 $_ => $p_res->vsattr($d->vendor_num('FreeRADIUS'), $_)->[0]
130 } $p_res->vsattributes($d->vendor_num('FreeRADIUS'));
134 warn "no answer, $!\n";
140 #wrappers for specific types of stats
141 sub radius_stats_get_global { return radius_stats_get(0x1f); }
142 sub radius_stats_get_client { return radius_stats_get(0x3f, 'FreeRADIUS-Stats-Client-Number' => $_[0]); }
145 #main loop of thread fetching status from freeradius server
150 INFO 'fetching new data';
151 my $main_stat = radius_stats_get_global();
153 if ( defined $main_stat ) {
154 my @clients_stat = ();
156 if ( $cfg->{clients} ) {
160 my $client_stat = radius_stats_get_client($client_id);
161 last unless exists $client_stat->{'FreeRADIUS-Stats-Client-IP-Address'};
162 push @clients_stat, $client_stat;
167 #todo ng: update on demand, and update only parts of snmp visible stats
169 INFO 'got data, updating stats';
170 radius_snmp_stats($main_stat, \@clients_stat);
173 WARN 'problem with fetching data';
177 INFO 'stats updated, sleeping';
178 sleep $cfg->{radius}->{refresh_rate};
183 #helper to get string from NetSNMP::OID
184 sub oid_s { return join '.', $_[0]->to_array; }
186 #handler for snmp requests from master agent(clients)
188 DEBUG 'got new request';
189 my ($handler, $registration_info, $request_info, $requests) = @_;
194 for ( my $request = $requests; $request; $request = $request->next() ) {
195 INFO 'request type '.$request_info->getMode.' for oid: '.oid_s($request->getOID);
197 if ( $request_info->getMode == MODE_GET ) {
198 my $oid_s = oid_s($request->getOID);
199 if ( exists $snmp_data{$oid_s} ) {
200 $request->setValue($snmp_data{$oid_s}->[0], ''.$snmp_data{$oid_s}->[1]);
203 }elsif ( $request_info->getMode == MODE_GETNEXT ) {
204 foreach my $oid ( @snmp_data_k ) {
205 #the keys is sorted in ascending order, so we are looking for
206 #first value bigger than one in request
207 if ( $request->getOID < NetSNMP::OID->new($oid) ) {
208 $request->setValue($snmp_data{$oid}->[0], ''.$snmp_data{$oid}->[1]);
209 $request->setOID($oid);
216 $request->setError($request_info, SNMP_ERR_READONLY);
220 DEBUG 'finished processing the request';
223 #init part of subtree for handling radius auth statistics
224 sub radius_snmp_stats_init_auth {
225 my ( $snmp_data_n, $oid, $clients ) = @_;
227 @{$snmp_data_n->{$oid.'.1.1.1.1'} = &share([])} = (ASN_OCTET_STR, ''); #radiusAuthServIdent
228 @{$snmp_data_n->{$oid.'.1.1.1.2'} = &share([])} = (ASN_TIMETICKS, 0); #radiusAuthServUpTime
229 @{$snmp_data_n->{$oid.'.1.1.1.3'} = &share([])} = (ASN_TIMETICKS, 0); #radiusAuthServResetTime
230 @{$snmp_data_n->{$oid.'.1.1.1.4'} = &share([])} = (ASN_INTEGER, 0); #radiusAuthServConfigReset
231 @{$snmp_data_n->{$oid.'.1.1.1.5'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalAccessRequests
232 @{$snmp_data_n->{$oid.'.1.1.1.6'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalInvalidRequests
233 @{$snmp_data_n->{$oid.'.1.1.1.7'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalDupAccessRequests
234 @{$snmp_data_n->{$oid.'.1.1.1.8'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalAccessAccepts
235 @{$snmp_data_n->{$oid.'.1.1.1.9'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalAccessRejects
236 @{$snmp_data_n->{$oid.'.1.1.1.10'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalAccessChallenges
237 @{$snmp_data_n->{$oid.'.1.1.1.11'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalMalformedAccessRequests
238 @{$snmp_data_n->{$oid.'.1.1.1.12'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalBadAuthenticators
239 @{$snmp_data_n->{$oid.'.1.1.1.13'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalPacketsDropped
240 @{$snmp_data_n->{$oid.'.1.1.1.14'} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServTotalUnknownTypes
242 #radiusAuthClientTable
243 for (1 .. scalar @$clients) {
244 @{$snmp_data_n->{$oid.'.1.1.1.15.1.1.'.$_} = &share([])} = (ASN_INTEGER, $_); #radiusAuthClientIndex
245 @{$snmp_data_n->{$oid.'.1.1.1.15.1.2.'.$_} = &share([])} = (ASN_IPADDRESS, pack 'C4', split /\./, $clients->[$_-1]->{'FreeRADIUS-Stats-Client-IP-Address'}); #radiusAuthClientAddress
246 @{$snmp_data_n->{$oid.'.1.1.1.15.1.3.'.$_} = &share([])} = (ASN_OCTET_STR, $clients->[$_-1]->{'FreeRADIUS-Stats-Client-Number'}); #radiusAuthClientID
247 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.4.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServAccessRequests
248 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.5.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServDupAccessRequests
249 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.6.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServAccessAccepts
250 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.7.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServAccessRejects
251 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.8.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServAccessChallenges
252 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.9.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServMalformedAccessRequests
253 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.10.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServBadAuthenticators
254 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.11.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServPacketsDropped
255 # @{$snmp_data_n->{$oid.'.1.1.1.15.1.12.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAuthServUnknownTypes
259 #init part of subtree for handling radius acct statistics
260 sub radius_snmp_stats_init_acct {
261 my ( $snmp_data_n, $oid, $clients ) = @_;
263 @{$snmp_data_n->{$oid.'.1.1.1.1'} = &share([])} = (ASN_OCTET_STR, ''); #radiusAccServIdent
264 @{$snmp_data_n->{$oid.'.1.1.1.2'} = &share([])} = (ASN_TIMETICKS, 0); #radiusAccServUpTime
265 @{$snmp_data_n->{$oid.'.1.1.1.3'} = &share([])} = (ASN_TIMETICKS, 0); #radiusAccServResetTime
266 @{$snmp_data_n->{$oid.'.1.1.1.4'} = &share([])} = (ASN_INTEGER, 0); #radiusAccServConfigReset
267 @{$snmp_data_n->{$oid.'.1.1.1.5'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalRequests
268 @{$snmp_data_n->{$oid.'.1.1.1.6'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalInvalidRequests
269 @{$snmp_data_n->{$oid.'.1.1.1.7'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalDupRequests
270 @{$snmp_data_n->{$oid.'.1.1.1.8'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalResponses
271 @{$snmp_data_n->{$oid.'.1.1.1.9'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalMalformedRequests
272 @{$snmp_data_n->{$oid.'.1.1.1.10'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalBadAuthenticators
273 @{$snmp_data_n->{$oid.'.1.1.1.11'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalPacketsDropped
274 @{$snmp_data_n->{$oid.'.1.1.1.12'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalNoRecords
275 @{$snmp_data_n->{$oid.'.1.1.1.13'} = &share([])} = (ASN_COUNTER, 0); #radiusAccServTotalUnknownTypes
277 #radiusAccClientTable
278 for (1 .. scalar @$clients) {
279 @{$snmp_data_n->{$oid.'.1.1.1.14.1.1.'.$_} = &share([])} = (ASN_INTEGER, $_); #radiusAccClientIndex
280 @{$snmp_data_n->{$oid.'.1.1.1.14.1.2.'.$_} = &share([])} = (ASN_IPADDRESS, pack 'C4', split /\./, $clients->[$_-1]->{'FreeRADIUS-Stats-Client-IP-Address'}); #radiusAccClientAddress
281 @{$snmp_data_n->{$oid.'.1.1.1.14.1.3.'.$_} = &share([])} = (ASN_OCTET_STR, $clients->[$_-1]->{'FreeRADIUS-Stats-Client-Number'}); #radiusAccClientID
282 # @{$snmp_data_n->{$oid.'.1.1.1.14.1.4.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAccServPacketsDropped
283 # @{$snmp_data_n->{$oid.'.1.1.1.14.1.5.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAccServRequests
284 # @{$snmp_data_n->{$oid.'.1.1.1.14.1.6.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAccServDupRequests
285 # @{$snmp_data_n->{$oid.'.1.1.1.14.1.7.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAccServResponses
286 # @{$snmp_data_n->{$oid.'.1.1.1.14.1.8.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAccServBadAuthenticators
287 # @{$snmp_data_n->{$oid.'.1.1.1.14.1.9.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAccServMalformedRequests
288 # @{$snmp_data_n->{$oid.'.1.1.1.14.1.10.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAccServNoRecords
289 # @{$snmp_data_n->{$oid.'.1.1.1.14.1.11.'.$_} = &share([])} = (ASN_COUNTER, 0); #radiusAccServUnknownTypes
293 #fill part of subtree with data from radius auth statistics
294 sub radius_snmp_stats_fill_auth {
295 my ( $snmp_data_n, $oid, $prefix, $main, $clients ) = @_;
300 $snmp_data_n->{$oid.'.1.1.1.1'}->[1] = 'snmp(over)radius';
301 $snmp_data_n->{$oid.'.1.1.1.2'}->[1] = ($time - $main->{'FreeRADIUS-Stats-Start-Time'})*100;
302 $snmp_data_n->{$oid.'.1.1.1.3'}->[1] = ($time - $main->{'FreeRADIUS-Stats-HUP-Time'})*100;
303 $snmp_data_n->{$oid.'.1.1.1.4'}->[1] = 0;
304 $snmp_data_n->{$oid.'.1.1.1.5'}->[1] += $main->{$prefix.'Access-Requests'};
305 $snmp_data_n->{$oid.'.1.1.1.6'}->[1] += $main->{$prefix.'Auth-Invalid-Requests'};
306 $snmp_data_n->{$oid.'.1.1.1.7'}->[1] += $main->{$prefix.'Auth-Duplicate-Requests'};
307 $snmp_data_n->{$oid.'.1.1.1.8'}->[1] += $main->{$prefix.'Access-Accepts'};
308 $snmp_data_n->{$oid.'.1.1.1.9'}->[1] += $main->{$prefix.'Access-Rejects'};
309 $snmp_data_n->{$oid.'.1.1.1.10'}->[1] += $main->{$prefix.'Access-Challenges'};
310 $snmp_data_n->{$oid.'.1.1.1.11'}->[1] += $main->{$prefix.'Auth-Malformed-Requests'};
311 $snmp_data_n->{$oid.'.1.1.1.12'}->[1] += 0;
312 $snmp_data_n->{$oid.'.1.1.1.13'}->[1] += $main->{$prefix.'Auth-Dropped-Requests'};
313 $snmp_data_n->{$oid.'.1.1.1.14'}->[1] += $main->{$prefix.'Auth-Unknown-Types'};
315 for (1 .. scalar @$clients) {
316 # $snmp_data_n->{$oid.'.1.1.1.15.1.4.'.$_}->[1] += $clients->[$_-1]->{$prefix.'Access-Requests'};
317 # $snmp_data_n->{$oid.'.1.1.1.15.1.5.'.$_}->[1] += $clients->[$_-1]->{$prefix.'Auth-Duplicate-Requests'};
318 # $snmp_data_n->{$oid.'.1.1.1.15.1.6.'.$_}->[1] += $clients->[$_-1]->{$prefix.'Access-Accepts'};
319 # $snmp_data_n->{$oid.'.1.1.1.15.1.7.'.$_}->[1] += $clients->[$_-1]->{$prefix.'Access-Rejects'};
320 # $snmp_data_n->{$oid.'.1.1.1.15.1.8.'.$_}->[1] += $clients->[$_-1]->{$prefix.'Access-Challenges'};
321 # $snmp_data_n->{$oid.'.1.1.1.15.1.9.'.$_}->[1] += $clients->[$_-1]->{$prefix.'Auth-Malformed-Requests'};
322 # $snmp_data_n->{$oid.'.1.1.1.15.1.10.'.$_}->[1] += 0;
323 # $snmp_data_n->{$oid.'.1.1.1.15.1.11.'.$_}->[1] += $clients->[$_-1]->{$prefix.'Auth-Dropped-Requests'};
324 # $snmp_data_n->{$oid.'.1.1.1.15.1.12.'.$_}->[1] += $clients->[$_-1]->{$prefix.'Auth-Unknown-Types'};
328 #fill part of subtree with data from radius acct statistics
329 sub radius_snmp_stats_fill_acct {
330 my ( $snmp_data_n, $oid, $prefix, $main, $clients ) = @_;
335 $snmp_data_n->{$oid.'.1.1.1.1'}->[1] = 'snmp(over)radius';
336 $snmp_data_n->{$oid.'.1.1.1.2'}->[1] = ($time - $main->{'FreeRADIUS-Stats-Start-Time'})*100;
337 $snmp_data_n->{$oid.'.1.1.1.3'}->[1] = ($time - $main->{'FreeRADIUS-Stats-HUP-Time'})*100;
338 $snmp_data_n->{$oid.'.1.1.1.4'}->[1] = 0;
339 $snmp_data_n->{$oid.'.1.1.1.5'}->[1] += $main->{$prefix.'Accounting-Requests'};
340 $snmp_data_n->{$oid.'.1.1.1.6'}->[1] += $main->{$prefix.'Acct-Invalid-Requests'};
341 $snmp_data_n->{$oid.'.1.1.1.7'}->[1] += $main->{$prefix.'Acct-Duplicate-Requests'};
342 $snmp_data_n->{$oid.'.1.1.1.8'}->[1] += $main->{$prefix.'Accounting-Responses'};
343 $snmp_data_n->{$oid.'.1.1.1.9'}->[1] += $main->{$prefix.'Acct-Malformed-Requests'};
344 $snmp_data_n->{$oid.'.1.1.1.10'}->[1] += 0;
345 $snmp_data_n->{$oid.'.1.1.1.11'}->[1] += $main->{$prefix.'Acct-Dropped-Requests'};
346 $snmp_data_n->{$oid.'.1.1.1.12'}->[1] += 0;
347 $snmp_data_n->{$oid.'.1.1.1.13'}->[1] += $main->{$prefix.'Acct-Unknown-Types'};
349 for (1 .. scalar @$clients) {
350 # $snmp_data_n->{$oid.'.1.1.1.14.1.4.'.$_}->[1] += $clients->[$_-1]->{$prefix.''};# 'radiusAccServPacketsDropped';
351 # $snmp_data_n->{$oid.'.1.1.1.14.1.5.'.$_}->[1] += $clients->[$_-1]->{$prefix.''};# 'radiusAccServRequests';
352 # $snmp_data_n->{$oid.'.1.1.1.14.1.6.'.$_}->[1] += $clients->[$_-1]->{$prefix.''};# 'radiusAccServDupRequests';
353 # $snmp_data_n->{$oid.'.1.1.1.14.1.7.'.$_}->[1] += $clients->[$_-1]->{$prefix.''};# 'radiusAccServResponses';
354 # $snmp_data_n->{$oid.'.1.1.1.14.1.8.'.$_}->[1] += $clients->[$_-1]->{$prefix.''};# 'radiusAccServBadAuthenticators';
355 # $snmp_data_n->{$oid.'.1.1.1.14.1.9.'.$_}->[1] += $clients->[$_-1]->{$prefix.''};# 'radiusAccServMalformedRequests';
356 # $snmp_data_n->{$oid.'.1.1.1.14.1.10.'.$_}->[1] += $clients->[$_-1]->{$prefix.''};# 'radiusAccServNoRecords';
357 # $snmp_data_n->{$oid.'.1.1.1.14.1.11.'.$_}->[1] += $clients->[$_-1]->{$prefix.''};# 'radiusAccServUnknownTypes';
362 sub radius_snmp_stats {
363 my ( $main, $clients ) = @_;
365 #print Dumper($main, $clients);
369 # we have to go through all oid's
370 foreach my $oid_s ( keys %{$cfg->{snmp}->{oid_sub}} ) {
372 #we're rebuilding the tree for data
373 #we could do it only once, but it will change when we will start handling more dynamic
375 my %types = map { $_ => 1 } map { /(?:proxy)?(\w+)/; $1 } @{$cfg->{snmp}->{oid_sub}->{$oid_s}};
376 WARN 'two conflicting types for oid '.$oid_s if scalar keys %types > 1;
378 if ( (keys %types)[0] eq 'auth' ) {
379 radius_snmp_stats_init_auth(\%snmp_data_n, $cfg->{snmp}->{oid_root}.'.'.$oid_s, $clients);
381 }elsif ( (keys %types)[0] eq 'acct' ) {
382 radius_snmp_stats_init_acct(\%snmp_data_n, $cfg->{snmp}->{oid_root}.'.'.$oid_s, $clients);
385 WARN 'unknown subtree type '.(keys %types)[0];
389 #now lets refill the statistics
390 foreach my $type ( @{$cfg->{snmp}->{oid_sub}->{$oid_s}} ) {
391 if ( $type eq 'auth' ) {
392 radius_snmp_stats_fill_auth(
393 \%snmp_data_n, $cfg->{snmp}->{oid_root}.'.'.$oid_s,
394 'FreeRADIUS-Total-', $main, $clients);
396 }elsif ( $type eq 'proxyauth' ) {
397 radius_snmp_stats_fill_auth(
398 \%snmp_data_n, $cfg->{snmp}->{oid_root}.'.'.$oid_s,
399 'FreeRADIUS-Total-Proxy-', $main, $clients);
401 }elsif ( $type eq 'acct' ) {
402 radius_snmp_stats_fill_acct(
403 \%snmp_data_n, $cfg->{snmp}->{oid_root}.'.'.$oid_s,
404 'FreeRADIUS-Total-', $main, $clients);
406 }elsif ( $type eq 'proxyacct' ) {
407 radius_snmp_stats_fill_acct(
408 \%snmp_data_n, $cfg->{snmp}->{oid_root}.'.'.$oid_s,
409 'FreeRADIUS-Total-Proxy-', $main, $clients);
412 WARN 'unknown subtree type '.$type;
418 #we rebuild the tree, so lets now lock the shared variables and push new data there
422 %snmp_data = %snmp_data_n;
423 @snmp_data_k = map { oid_s($_) } sort { $a <=> $b } map { NetSNMP::OID->new($_) } keys %snmp_data_n;
428 freeradius snmp agentx subagent
434 make sure snmpd is agentx master (snmpd.conf):
437 run the script (no demonizing support yet):
441 then you can walk the tree (default oid):
443 snmpbulkwalk -On -v2c -cpublic localhost .1.3.6.1.2.1.67
449 Net-Radius (either 1.56 + net-radius-freeradius-dictionary.diff to use freeradius dictionaries
450 or vanilla upstream one + dictionary.hacked)
451 NetSNMP perl modules (available with net-snmp distribution)
457 Stanislaw Sawa <stanislaw.sawa(at)sns.bskyb.com>
461 Copyright (C) 2008 Sky Network Services. All Rights Reserved.
463 This program is free software; you can redistribute it and/or modify it
464 under the same terms as Perl itself.