File Coverage

blib/lib/POE/Component/Server/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::Server::RADIUS;
2             {
3             $POE::Component::Server::RADIUS::VERSION = '1.08';
4             }
5              
6             #ABSTRACT: a POE based RADIUS server component
7              
8 8     8   282590 use strict;
  8         22  
  8         584  
9 8     8   42 use warnings;
  8         232  
  8         599  
10 8     8   10510 use Socket;
  8         49532  
  8         6532  
11 8     8   4155 use POE;
  0            
  0            
12             use Net::Radius::Dictionary;
13             use Net::Radius::Packet;
14             use Net::IP::Minimal qw(ip_is_ipv4);
15              
16             use constant DATAGRAM_MAXLEN => 4096;
17             use constant RADIUS_PORT => 1812;
18             use constant ACCOUNTING_PORT => 1813;
19             use constant RADIUS_PORT_OLD => 1645;
20             use constant ACCOUNTING_PORT_OLD => 1646;
21              
22             # Stolen from POE::Wheel. This is static data, shared by all
23             my $current_id = 0;
24             my %active_identifiers;
25              
26             sub spawn {
27             my $package = shift;
28             my %opts = @_;
29             $opts{lc $_} = delete $opts{$_} for keys %opts;
30             unless ( ref $opts{dict} and $opts{dict}->isa('Net::Radius::Dictionary') ) {
31             warn "No 'dict' object provided, bailing out\n";
32             return;
33             }
34             my $options = delete $opts{options};
35             my $self = bless \%opts, $package;
36             $self->{session_id} = POE::Session->create(
37             object_states => [
38             $self => { shutdown => '_shutdown',
39             accept => '_command',
40             reject => '_command',
41             },
42             $self => [qw(_start _get_auth_data _get_acct_data register unregister _req_alarm)],
43             ],
44             heap => $self,
45             ( ref($options) eq 'HASH' ? ( options => $options ) : () ),
46             )->ID();
47             return $self;
48             }
49              
50             sub _allocate_identifier {
51             while (1) {
52             last unless exists $active_identifiers{ ++$current_id };
53             }
54             return $active_identifiers{$current_id} = $current_id;
55             }
56              
57             sub _free_identifier {
58             my $id = shift;
59             delete $active_identifiers{$id};
60             }
61              
62             sub session_id {
63             return $_[0]->{session_id};
64             }
65              
66             sub shutdown {
67             my $self = shift;
68             $poe_kernel->post( $self->{session_id}, 'shutdown' );
69             }
70              
71             sub dictionary {
72             return $_[0]->{dict};
73             }
74              
75             sub add_client {
76             my $self = shift;
77             my %opts = @_;
78             $opts{lc $_} = delete $opts{$_} for keys %opts;
79             unless ( $opts{name} and $opts{address} and $opts{secret} ) {
80             warn "You must provide a 'name' and 'address' and 'secret'\n";
81             return;
82             }
83             unless ( ip_is_ipv4( $opts{address} ) ) {
84             warn "'address' must be an IPv4 address\n";
85             return;
86             }
87             if ( $self->{clients}->{ $opts{name} } ) {
88             warn "That 'name' already exists\n";
89             return;
90             }
91             if ( grep { $self->{clients}->{$_}->{address} eq $opts{address} } keys %{ $self->{clients} } ) {
92             warn "That 'address' already exists\n";
93             return;
94             }
95             $self->{clients}->{ $opts{name} }->{$_} = $opts{$_} for qw(address secret);
96             return 1;
97             }
98              
99             sub del_client {
100             my $self = shift;
101             my $value = shift || return;
102             if ( $self->{clients}->{ $value } ) {
103             delete $self->{clients}->{ $value };
104             return 1;
105             }
106             if ( ip_is_ipv4( $value ) ) {
107             foreach my $name ( keys %{ $self->{clients} } ) {
108             next unless $self->{clients}->{$name}->{address} eq $value;
109             delete $self->{clients}->{$name};
110             return 1;
111             }
112             }
113             return;
114             }
115              
116             sub _validate_client {
117             my $self = shift;
118             my $client = shift || return;
119             foreach my $name ( keys %{ $self->{clients} } ) {
120             next unless $self->{clients}->{$name}->{address} eq $client;
121             return $self->{clients}->{$name}->{secret};
122             }
123             return;
124             }
125              
126             sub authports {
127             my $self = shift;
128             return map { ( sockaddr_in( getsockname $_ ) )[0] } @{ $self->{_authsocks} };
129             }
130              
131             sub acctports {
132             my $self = shift;
133             return map { ( sockaddr_in( getsockname $_ ) )[0] } @{ $self->{_acctsocks} };
134             }
135              
136             sub _start {
137             my ($kernel,$self) = @_[KERNEL,OBJECT];
138             $self->{session_id} = $_[SESSION]->ID();
139             $kernel->alias_set( $self->{alias} ) if $self->{alias};
140             $kernel->refcount_increment($self->{session_id}, __PACKAGE__) unless $self->{alias};
141             my @authports; my @acctports;
142             push @authports, $self->{authport} if defined $self->{authport};
143             push @acctports, $self->{acctport} if defined $self->{acctport};
144             unless ( defined $self->{authport} ) {
145             push @authports, RADIUS_PORT;
146             push @authports, RADIUS_PORT_OLD if $self->{legacy};
147             }
148             unless ( defined $self->{acctport} ) {
149             push @acctports, ACCOUNTING_PORT;
150             push @acctports, ACCOUNTING_PORT_OLD if $self->{legacy};
151             }
152             my $proto = getprotobyname('udp');
153             foreach my $port ( @authports ) {
154             my $paddr = sockaddr_in($port, INADDR_ANY);
155             socket( my $socket, PF_INET, SOCK_DGRAM, $proto);
156             bind( $socket, $paddr);
157             push @{ $self->{_authsocks} }, $socket;
158             $kernel->select_read( $socket, '_get_auth_data' );
159             }
160             foreach my $port ( @acctports ) {
161             my $paddr = sockaddr_in($port, INADDR_ANY);
162             socket( my $socket, PF_INET, SOCK_DGRAM, $proto);
163             bind( $socket, $paddr);
164             push @{ $self->{_acctsocks} }, $socket;
165             $kernel->select_read( $socket, '_get_acct_data' );
166             }
167             return;
168             }
169              
170             sub _shutdown {
171             my ($kernel,$self) = @_[KERNEL,OBJECT];
172             $kernel->alarm_remove_all();
173             $kernel->alias_remove( $_ ) for $kernel->alias_list();
174             $kernel->refcount_decrement($self->{session_id}, __PACKAGE__) unless $self->{alias};
175             $kernel->select_read( $_ ) for @{ $self->{_authsocks} };
176             $kernel->select_read( $_ ) for @{ $self->{_acctsocks} };
177             delete $self->{_authsocks}; delete $self->{_acctsocks};
178             delete $self->{_requests};
179             $kernel->refcount_decrement( $_, __PACKAGE__ ) for keys %{ $self->{sessions} };
180             return;
181             }
182              
183             sub _get_auth_data {
184             my ($kernel,$self,$socket) = @_[KERNEL,OBJECT,ARG0];
185             my $remote_address = recv( $socket, my $message = '', 4096, 0 );
186             # Check remote_address is valid
187             my $client = inet_ntoa( ( sockaddr_in $remote_address )[1] );
188             my $secret = $self->_validate_client( $client );
189             return unless $secret;
190             my $p = Net::Radius::Packet->new( $self->{dict}, $message );
191             # Check $p is valid
192             return unless $p->code eq 'Access-Request';
193             my $data = {
194             map { ( $_, $p->attr($_) ) } $p->attributes()
195             };
196             $data->{'User-Password'} = $p->password( $secret ) if $data->{'User-Password'};
197             my $req_id = _allocate_identifier();
198             $self->{_requests}->{ $req_id } = {
199             identifier => $p->identifier,
200             authenticator => $p->authenticator,
201             from => $remote_address,
202             client => $client,
203             secret => $secret,
204             socket => $socket,
205             };
206             # dispatch to interested sessions
207             $kernel->post( $_, $self->{sessions}->{$_}->{authevent}, $client, $data, $req_id, $p )
208             for grep { $self->{sessions}->{$_}->{authevent} } keys %{ $self->{sessions} };
209             # set an alarm
210             $self->{_requests}->{ $req_id }->{alarm_id} =
211             $kernel->delay_set( '_req_alarm', $self->{timeout} || 10, $req_id );
212             return;
213             }
214              
215             sub _req_alarm {
216             my ($kernel,$self,$req_id) = @_[KERNEL,OBJECT,ARG0];
217             return unless $self->{_requests}->{ $req_id };
218             delete $self->{_requests}->{ $req_id };
219             _free_identifier( $req_id );
220             return;
221             }
222              
223             sub _get_acct_data {
224             my ($kernel,$self,$socket) = @_[KERNEL,OBJECT,ARG0];
225             my $remote_address = recv( $socket, my $message = '', 4096, 0 );
226             # Check remote_address is valid
227             my $client = inet_ntoa( ( sockaddr_in $remote_address )[1] );
228             my $secret = $self->_validate_client( $client );
229             return unless $secret;
230             return unless auth_acct_verify( $message, $secret );
231             my $p = Net::Radius::Packet->new( $self->{dict}, $message );
232             # Check $p is valid
233             return unless $p->code eq 'Accounting-Request';
234             my $data = {
235             map { ( $_, $p->attr($_) ) } $p->attributes()
236             };
237             # dispatch to interested sessions
238             $kernel->post( $_, $self->{sessions}->{$_}->{acctevent}, $client, $data, $p )
239             for grep { $self->{sessions}->{$_}->{acctevent} } keys %{ $self->{sessions} };
240             my $rp = Net::Radius::Packet->new( $self->{dict} );
241             $rp->set_identifier($p->identifier);
242             $rp->set_authenticator($p->authenticator);
243             $rp->set_code('Accounting-Response');
244             my $reply = auth_resp( $rp->pack, $secret );
245             warn "Problem sending packet to '$client'\n" unless
246             send( $socket, $reply, 0, $remote_address ) == length($reply);
247             return;
248             }
249              
250             sub _command {
251             my ($kernel,$self,$state,$req_id) = @_[KERNEL,OBJECT,STATE,ARG0];
252             my %args;
253             if ( ref $_[ARG1] eq 'HASH' ) {
254             %args = %{ $_[ARG1] };
255             }
256             elsif ( ref $_[ARG1] eq 'ARRAY' ) {
257             %args = @{ $_[ARG1] };
258             }
259             else {
260             %args = @_[ARG1..$#_];
261             }
262             return unless $self->{_requests}->{ $req_id };
263             my $req = delete $self->{_requests}->{ $req_id };
264             _free_identifier( $req_id );
265             $kernel->alarm_remove( $req->{alarm_id} );
266             my $code;
267             $code = 'Access-Accept' if $state eq 'accept';
268             $code = 'Access-Reject' if $state eq 'reject';
269             my $rp = Net::Radius::Packet->new( $self->{dict} );
270             $rp->set_identifier( $req->{identifier} );
271             $rp->set_authenticator( $req->{authenticator} );
272             $rp->set_code( $code );
273             $rp->set_attr( $_, $args{$_} ) for keys %args;
274             my $reply = auth_resp( $rp->pack, $req->{secret} );
275             warn "Problem sending packet to '$req->{client}'\n" unless
276             send( $req->{socket}, $reply, 0, $req->{from} ) == length($reply);
277             return;
278             }
279              
280             sub register {
281             my ($kernel,$self,$sender) = @_[KERNEL,OBJECT,SENDER];
282             my $sender_id = $sender->ID();
283             my %args;
284             if ( ref $_[ARG0] eq 'HASH' ) {
285             %args = %{ $_[ARG0] };
286             }
287             elsif ( ref $_[ARG0] eq 'ARRAY' ) {
288             %args = @{ $_[ARG0] };
289             }
290             else {
291             %args = @_[ARG0..$#_];
292             }
293             $args{lc $_} = delete $args{$_} for keys %args;
294             unless ( $args{authevent} or $args{acctevent} ) {
295             warn "You must specify either 'authevent' or 'acctevent' arguments\n";
296             return;
297             }
298             if ( defined $self->{sessions}->{ $sender_id } ) {
299             $self->{sessions}->{ $sender_id } = \%args;
300             }
301             else {
302             $self->{sessions}->{ $sender_id } = \%args;
303             $kernel->refcount_increment( $sender_id, __PACKAGE__ );
304             }
305             return;
306             }
307              
308             sub unregister {
309             my ($kernel,$self,$sender) = @_[KERNEL,OBJECT,SENDER];
310             my $sender_id = $sender->ID();
311             my %args;
312             if ( ref $_[ARG0] eq 'HASH' ) {
313             %args = %{ $_[ARG0] };
314             }
315             elsif ( ref $_[ARG0] eq 'ARRAY' ) {
316             %args = @{ $_[ARG0] };
317             }
318             else {
319             %args = @_[ARG0..$#_];
320             }
321             $args{lc $_} = delete $args{$_} for keys %args;
322             my $data = delete $self->{sessions}->{ $sender_id };
323             $kernel->refcount_decrement( $sender_id, __PACKAGE__ ) if $data;
324             return;
325             }
326              
327             1;
328              
329             __END__