File Coverage

blib/lib/POE/Component/Client/RADIUS.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package POE::Component::Client::RADIUS;
2             $POE::Component::Client::RADIUS::VERSION = '1.04';
3             #ABSTRACT: a flexible POE-based RADIUS client
4              
5 11     11   361539 use strict;
  11         388  
  11         468  
6 11     11   62 use warnings;
  11         26  
  11         413  
7 11     11   63 use Carp;
  11         30  
  11         1060  
8 11     11   6002 use POE;
  0            
  0            
9             use IO::Socket::INET;
10             use Net::Radius::Dictionary;
11             use Net::Radius::Packet;
12             use Math::Random;
13              
14             use constant DATAGRAM_MAXLEN => 4096;
15             use constant RADIUS_PORT => 1812;
16             use constant ACCOUNTING_PORT => 1813;
17              
18             my $ERROR;
19             my $ERRNO;
20              
21             # Stolen from POE::Wheel. This is static data, shared by all
22             my $current_id = 0;
23             my %active_identifiers;
24              
25             sub spawn {
26             my $package = shift;
27             return $package->_create( 'spawn', @_ );
28             }
29              
30             sub authenticate {
31             my $self;
32             eval {
33             if ( (ref $_[0]) && $_[0]->isa(__PACKAGE__) ) {
34             $self = shift;
35             }
36             };
37             if ( $self ) {
38             $poe_kernel->post( $self->{session_id}, 'authenticate', @_ );
39             return 1;
40             }
41             my $package = shift;
42             return $package->_create( 'authenticate', @_ );
43             }
44              
45             sub accounting {
46             my $self;
47             eval {
48             if ( (ref $_[0]) && $_[0]->isa(__PACKAGE__) ) {
49             $self = shift;
50             }
51             };
52             if ( $self ) {
53             $poe_kernel->post( $self->{session_id}, 'accounting', @_ );
54             return 1;
55             }
56             my $package = shift;
57             return $package->_create( 'accounting', @_ );
58             }
59              
60             sub _create {
61             my $package = shift;
62             my $command = shift;
63             my %opts = @_;
64             $opts{lc $_} = delete $opts{$_} for grep { !/^_/ } keys %opts;
65             unless ( ref $opts{dict} and $opts{dict}->isa('Net::Radius::Dictionary') ) {
66             warn "No 'dict' object provided, bailing out\n";
67             return;
68             }
69             my $options = delete $opts{options};
70             my $self = bless { }, $package;
71             if ( $command =~ /^a/ ) {
72             unless ( $opts{event} ) {
73             warn "You must specify 'event' for '$command'\n";
74             return;
75             }
76             unless ( $opts{server} and _ip_is_v4( $opts{server} ) ) {
77             warn "You must specify 'server' as a valid IPv4 address\n";
78             return;
79             }
80             unless ( $opts{secret} ) {
81             warn "You must specify a 'secret'\n";
82             return;
83             }
84             unless ( $opts{attributes} and ref $opts{attributes} eq 'HASH' ) {
85             warn "You must specify 'attributes' as a hashref of RADIUS attributes\n";
86             return;
87             }
88             if ( $command eq 'authenticate' and !( $opts{username} and $opts{password} ) ) {
89             warn "You must specify 'username' and 'password' for 'authenticate'\n";
90             return;
91             }
92             if ( $command eq 'accounting' and !$opts{type} ) {
93             warn "You must specify 'type' for an accounting request\n";
94             return;
95             }
96             $opts{port} = RADIUS_PORT if $command eq 'authenticate' and !$opts{port};
97             $opts{port} = ACCOUNTING_PORT if $command eq 'accounting' and !$opts{port};
98             }
99             $self->{session_id} = POE::Session->create(
100             object_states => [
101             $self => { shutdown => '_shutdown',
102             authenticate => '_command',
103             accounting => '_command', },
104             $self => [qw(_start _create_socket _dispatch _get_datagram _sock_timeout)],
105             ],
106             heap => $self,
107             args => [ $command, %opts ],
108             ( ref($options) eq 'HASH' ? ( options => $options ) : () ),
109             )->ID();
110             return $self;
111             }
112              
113             sub _allocate_identifier {
114             while (1) {
115             ++$current_id;
116             $current_id = 1 if $current_id > 255;
117             last unless exists $active_identifiers{ $current_id };
118             }
119             return $active_identifiers{$current_id} = $current_id;
120             }
121              
122             sub _free_identifier {
123             my $id = shift;
124             delete $active_identifiers{$id};
125             }
126              
127             sub session_id {
128             return $_[0]->{session_id};
129             }
130              
131             sub shutdown {
132             my $self = shift;
133             $poe_kernel->post( $self->{session_id}, 'shutdown' );
134             }
135              
136             sub _start {
137             my ($kernel,$self,$sender,$command,@args) = @_[KERNEL,OBJECT,SENDER,ARG0..$#_];
138             $self->{session_id} = $_[SESSION]->ID();
139             if ( $command eq 'spawn' ) {
140             my $opts = { @args };
141             $self->{$_} = $opts->{$_} for keys %{ $opts };
142             $kernel->alias_set($self->{alias}) if $self->{alias};
143             $kernel->refcount_increment($self->{session_id}, __PACKAGE__) unless $self->{alias};
144             return;
145             }
146             if ( $kernel == $sender ) {
147             croak "'authenticate' and 'accounting' should be called from another POE Session\n";
148             }
149             $self->{sender_id} = $sender->ID();
150             $kernel->refcount_increment( $self->{sender_id}, __PACKAGE__ );
151             $kernel->yield( $command, @args );
152             return;
153             }
154              
155             sub _shutdown {
156             my ($kernel,$self) = @_[KERNEL,OBJECT];
157             $kernel->alarm_remove_all();
158             $kernel->alias_remove( $_ ) for $kernel->alias_list();
159             $kernel->refcount_decrement( $self->{session_id} => __PACKAGE__ ) unless $self->{alias};
160             return;
161             }
162              
163             sub _command {
164             my ($kernel,$self,$state,$session,$sender) = @_[KERNEL,OBJECT,STATE,SESSION,SENDER];
165             my $args;
166             if ( ref $_[ARG0] eq 'HASH' ) {
167             $args = $_[ARG0];
168             }
169             else {
170             $args = { @_[ARG0..$#_] };
171             }
172             $args->{cmd} = $state;
173             if ( $session == $sender ) {
174             $args->{sender_id} = $self->{sender_id};
175             $self->{dict} = delete $args->{dict};
176             }
177             else {
178             $args->{lc $_} = delete $args->{$_} for grep { !/^_/ } keys %{ $args };
179             $args->{sender_id} = $sender->ID();
180             unless ( $args->{event} ) {
181             warn "You must specify 'SuccessEvent' and 'FailureEvent' for '$state'\n";
182             return;
183             }
184             unless ( $args->{server} and _ip_is_v4( $args->{server} ) ) {
185             warn "You must specify 'server' as a valid IPv4 address\n";
186             return;
187             }
188             unless ( $args->{secret} ) {
189             warn "You must specify a 'secret'\n";
190             return;
191             }
192             unless ( $args->{attributes} and ref $args->{attributes} eq 'HASH' ) {
193             warn "You must specify 'attributes' as a hashref of RADIUS attributes\n";
194             return;
195             }
196             if ( $state eq 'authenticate' and !( $args->{username} and $args->{password} ) ) {
197             warn "You must specify 'username' and 'password' for 'authenticate'\n";
198             return;
199             }
200             if ( $state eq 'accounting' and !$args->{type} ) {
201             warn "You must specify 'type' for an accounting request\n";
202             return;
203             }
204             $args->{port} = RADIUS_PORT if $state eq 'authenticate' and !$args->{port};
205             $args->{port} = ACCOUNTING_PORT if $state eq 'accounting' and !$args->{port};
206             $kernel->refcount_increment( $args->{sender_id}, __PACKAGE__ );
207             }
208             my $req = Net::Radius::Packet->new( $self->{dict} );
209             my $packet;
210             if ( $state eq 'authenticate' ) {
211             $args->{identifier} = _allocate_identifier();
212             $args->{authenticator} = _bigrand();
213             $req->set_code('Access-Request');
214             $req->set_attr('User-Name' => $args->{username});
215             $req->set_attr('Service-Type' => '2');
216             $req->set_attr('Framed-Protocol' => 'PPP');
217             $req->set_attr('NAS-Port' => 1234);
218             $req->set_attr('NAS-Identifier' => 'PoCoClientRADIUS');
219             $req->set_attr('NAS-IP-Address' => _my_address( $args->{server} ) );
220             $req->set_attr('Called-Station-Id' => '0000');
221             $req->set_attr('Calling-Station-Id' => '01234567890');
222             delete $args->{attributes}->{'User-Name'};
223             $req->set_attr( $_ => $args->{attributes}->{$_} ) for keys %{ $args->{attributes} };
224             $req->set_identifier( $args->{identifier} );
225             $req->set_authenticator( $args->{authenticator} );
226             $req->set_password( $args->{password}, $args->{secret} );
227             $packet = $req->pack;
228             }
229             if ( $state eq 'accounting' ) {
230             $args->{identifier} = _allocate_identifier();
231             $args->{authenticator} = '';
232             $req->set_code('Accounting-Request');
233             $req->set_attr('Acct-Status-Type', ucfirst lc $args->{type});
234             delete $args->{attributes}->{'Acct-Status-Type'};
235             $req->set_attr( $_ => $args->{attributes}->{$_} ) for keys %{ $args->{attributes} };
236             $req->set_identifier( $args->{identifier} );
237             $req->set_authenticator( $args->{authenticator} );
238             $packet = auth_resp($req->pack,$args->{secret});
239             }
240             $kernel->yield( '_create_socket', $packet, $args );
241             return;
242             }
243              
244             sub _create_socket {
245             my ($kernel,$self,$packet,$data) = @_[KERNEL,OBJECT,ARG0,ARG1];
246             my $socket = IO::Socket::INET->new( Proto => 'udp' );
247             $kernel->select_read( $socket, '_get_datagram', $data );
248             unless ( $socket ) {
249             $data->{error} = $!;
250             $kernel->yield( '_dispatch', $data );
251             return;
252             }
253             my $server_address = pack_sockaddr_in( $data->{port}, inet_aton($data->{server}) );
254             unless ( $server_address ) {
255             $data->{error} = 'Couldn\'t create packed server address and socket';
256             $kernel->yield( '_dispatch', $data );
257             return;
258             }
259             unless ( send( $socket, $packet, 0, $server_address ) == length($packet) ) {
260             $data->{error} = $!;
261             $kernel->yield( '_dispatch', $data );
262             return;
263             }
264             $data->{alarm_id} = $kernel->delay_set( '_sock_timeout', $self->{timeout} || 10, $socket, $data );
265             return;
266             }
267              
268             sub _sock_timeout {
269             my ($kernel,$self,$socket,$data) = @_[KERNEL,OBJECT,ARG0,ARG1];
270             $kernel->select_read( $socket );
271             $data->{timeout} = 'Timeout waiting for a response';
272             $kernel->yield( '_dispatch', $data );
273             return;
274             }
275              
276             sub _get_datagram {
277             my ($kernel,$self,$socket,$data) = @_[KERNEL,OBJECT,ARG0,ARG2];
278             $kernel->alarm_remove( delete $data->{alarm_id} );
279             $kernel->select_read( $socket );
280             my $remote_address = recv( $socket, my $message = '', 4096, 0 );
281             unless ( defined $remote_address ) {
282             $data->{error} = $!;
283             $kernel->yield( '_dispatch', $data );
284             return;
285             }
286             my $resp = Net::Radius::Packet->new( $self->{dict}, $message );
287             my ($port, $iaddr) = unpack_sockaddr_in( $remote_address );
288             $iaddr = inet_ntoa( $iaddr );
289             if ( $data->{identifier} ne $resp->identifier or $iaddr ne $data->{server} ) {
290             $data->{error} = 'Unexpected response to request.';
291             $kernel->yield( '_dispatch', $data );
292             return;
293             }
294             if ( $data->{cmd} eq 'authenticate' and !auth_req_verify( $message, $data->{secret}, $data->{authenticator} ) ) {
295             $data->{error} = 'Couldn\'t authenticate the response from the server.';
296             $kernel->yield( '_dispatch', $data );
297             return;
298             }
299             my $reply = {
300             map { ( $_, $resp->attr($_) ) } $resp->attributes()
301             };
302             $reply->{Code} = $resp->code;
303             $data->{response} = $reply;
304             $kernel->yield( '_dispatch', $data );
305             return;
306             }
307              
308             sub _dispatch {
309             my ($kernel,$self,$data) = @_[KERNEL,OBJECT,ARG0];
310             delete $data->{authenticator};
311             my $ident = delete $data->{identifier};
312             _free_identifier( $ident ) if $ident;
313             $kernel->post( $data->{sender_id}, $data->{event}, $data );
314             $kernel->refcount_decrement( delete $data->{sender_id}, __PACKAGE__ );
315             return;
316             }
317              
318             #------------------------------------------------------------------------------
319             # Subroutine _ip_is_ipv4
320             # Purpose : Check if an IP address is version 4
321             # Params : IP address
322             # Returns : 1 (yes) or 0 (no)
323             sub _ip_is_v4 {
324             my $ip = shift;
325              
326             # Check for invalid chars
327             unless ($ip =~ m/^[\d\.]+$/) {
328             $ERROR = "Invalid chars in IP $ip";
329             $ERRNO = 107;
330             return 0;
331             }
332              
333             if ($ip =~ m/^\./) {
334             $ERROR = "Invalid IP $ip - starts with a dot";
335             $ERRNO = 103;
336             return 0;
337             }
338              
339             if ($ip =~ m/\.$/) {
340             $ERROR = "Invalid IP $ip - ends with a dot";
341             $ERRNO = 104;
342             return 0;
343             }
344              
345             # Single Numbers are considered to be IPv4
346             if ($ip =~ m/^(\d+)$/ and $1 < 256) { return 1 }
347              
348             # Count quads
349             my $n = ($ip =~ tr/\./\./);
350              
351             # IPv4 must have from 1 to 4 quads
352             unless ($n >= 0 and $n < 4) {
353             $ERROR = "Invalid IP address $ip";
354             $ERRNO = 105;
355             return 0;
356             }
357              
358             # Check for empty quads
359             if ($ip =~ m/\.\./) {
360             $ERROR = "Empty quad in IP address $ip";
361             $ERRNO = 106;
362             return 0;
363             }
364              
365             foreach (split /\./, $ip) {
366              
367             # Check for invalid quads
368             unless ($_ >= 0 and $_ < 256) {
369             $ERROR = "Invalid quad in IP address $ip - $_";
370             $ERRNO = 107;
371             return 0;
372             }
373             }
374             return 1;
375             }
376              
377             sub _bigrand {
378             my @numbers;
379             push @numbers, scalar random_uniform_integer(1,0,65536) for 0 .. 7;
380             pack "n8", @numbers;
381             }
382              
383             sub _my_address {
384             my $remote = shift || '198.41.0.4';
385             my $socket = IO::Socket::INET->new(
386             Proto => 'udp',
387             PeerAddr => $remote,
388             PeerPort => 53,
389             );
390             return unless $socket;
391             return $socket->sockhost;
392             }
393              
394             qq[Sound of crickets];
395              
396             __END__