File Coverage

blib/lib/AnyEvent/Radius/Client.pm
Criterion Covered Total %
statement 96 161 59.6
branch 13 56 23.2
condition 4 9 44.4
subroutine 19 31 61.2
pod 10 13 76.9
total 142 270 52.5


line stmt bran cond sub pod time code
1             package AnyEvent::Radius::Client;
2             # AnyEvent-based radius client
3 1     1   65023 use strict;
  1         3  
  1         24  
4 1     1   4 use warnings;
  1         3  
  1         25  
5 1     1   4 use AnyEvent;
  1         1  
  1         15  
6 1     1   410 use AnyEvent::Handle::UDP;
  1         27456  
  1         37  
7              
8 1     1   7 use base qw(Class::Accessor::Fast);
  1         2  
  1         401  
9             __PACKAGE__->mk_accessors(qw(
10             handler packer send_cache
11             queue_cv write_cv read_cv
12             sent_cnt reply_cnt queue_cnt
13             last_request_id
14             ));
15              
16 1     1   2704 use Data::Radius::Constants qw(%RADIUS_PACKET_TYPES);
  1         6967  
  1         122  
17 1     1   416 use Data::Radius::Dictionary ();
  1         9774  
  1         23  
18 1     1   530 use Data::Radius::Packet ();
  1         9783  
  1         33  
19              
20             use constant {
21 1         1432 READ_TIMEOUT_SEC => 5,
22             WRITE_TIMEOUT_SEC => 5,
23             RADIUS_PORT => 1812,
24             MAX_QUEUE => 255,
25 1     1   7 };
  1         2  
26              
27             # new 'NAS'
28             # args:
29             # ip
30             # port
31             # secret
32             # dictionary
33             # read_timeout
34             # write_timeout
35             # bind_ip
36             #- callbacks:
37             # on_read
38             # on_read_raw
39             # on_read_timeout
40             # on_write_timeout
41             # on_error
42             sub new {
43 1     1 1 1001317 my ($class, %h) = @_;
44              
45 1         22 my $obj = bless {}, $class;
46              
47             # either pre-created packer object, or need radius secret to create new one
48             # dictionary is optional
49 1 50       28 if ( defined $h{packer} ) {
    50          
50 0         0 $obj->packer( $h{packer} );
51             } elsif ( defined $h{secret} ) {
52 1         51 $obj->packer( Data::Radius::Packet->new(dict => $h{dictionary}, secret => $h{secret}) );
53             } else {
54 0         0 die "No radius secret";
55             }
56              
57             my %udp_handle_args = (
58             rtimeout => $h{read_timeout} // READ_TIMEOUT_SEC,
59 1   50     214 wtimeout => $h{write_timeout} // WRITE_TIMEOUT_SEC,
      50        
60             );
61              
62 1 50       12 die "No IP argument" if ! exists $h{ip};
63 1   50     12 $udp_handle_args{connect} = [ $h{ip}, $h{port} // RADIUS_PORT ];
64 1 50       11 $udp_handle_args{bind} = [$h{bind_ip}, 0] if exists $h{bind_ip};
65              
66             $udp_handle_args{on_recv} = sub {
67 2     2   1791 my ($data, $handle, $from) = @_;
68 2         49 $obj->read_cv->end;
69 2         63 $obj->reply_cnt($obj->reply_cnt + 1);
70              
71 2 50       21 if ($h{on_read_raw}) {
72             # dump raw data
73 0         0 $h{on_read_raw}->($obj, $data, $from);
74             }
75              
76             # using authenticator from request to verify reply
77 2         31 my $request_id = $obj->packer()->request_id($data);
78             # FIXME how to react on unknown request_id ?
79 2         61 my $send_info = delete $obj->send_cache()->{ $request_id };
80 2 50       17 if (! $send_info ) {
81             # got unknown reply (with wrong request id?)
82 0 0       0 if ($h{on_error}) {
83 0         0 $h{on_error}->($obj, 'Unknown reply');
84             }
85             else {
86 0         0 warn "Error: unknown reply";
87             }
88             }
89             else {
90 2         3 my $on_read = $h{on_read};
91 2         5 my $req_callback = $send_info->{callback};
92 2 50 33     7 if ( $on_read || $req_callback ) {
93             # how to decode $from
94             # my($port, $host) = AnyEvent::Socket::unpack_sockaddr($from);
95             # my $ip = format_ipv4($host);
96              
97 2         29 my ($type, $req_id, $auth, $av_list) = $obj->packer()->parse($data, $send_info->{authenticator});
98              
99 2 50       377 $on_read->($obj, {
100             type => $type,
101             request_id => $req_id,
102             av_list => $av_list,
103             # from is sockaddr binary data
104             from => $from,
105             authenticator => $auth,
106             }) if $on_read;
107 2 50       1651 $req_callback->($type, $av_list) if $req_callback;
108             }
109             }
110              
111 2         45 $obj->queue_cv->end;
112 1         33 };
113              
114             $udp_handle_args{on_rtimeout} = sub {
115 0     0   0 my $handle = shift;
116 0 0       0 if(! $obj->read_cv->ready) {
117 0 0       0 if($h{on_read_timeout}) {
118 0         0 $h{on_read_timeout}->($obj, $handle);
119             }
120 0         0 $obj->clear_send_cache();
121             # stop queue
122 0         0 $obj->queue_cv->send;
123             }
124 0         0 $handle->clear_rtimeout();
125 1         42 };
126              
127             $udp_handle_args{on_wtimeout} = sub {
128 0     0   0 my $handle = shift;
129 0 0       0 if(! $obj->write_cv->ready) {
130 0 0       0 if($h{on_write_timeout}) {
131 0         0 $h{on_write_timeout}->($obj, $handle);
132             }
133 0         0 $obj->clear_send_cache();
134             # stop queue
135 0         0 $obj->queue_cv->send;
136             }
137 0         0 $handle->clear_wtimeout();
138 1         26 };
139              
140             # low-level socket errors
141             $udp_handle_args{on_error} = sub {
142 0     0   0 my ($handle, $fatal, $error) = @_;
143             # abort all
144 0         0 $handle->clear_wtimeout();
145 0         0 $handle->clear_rtimeout();
146 0         0 $obj->clear_send_cache();
147 0         0 $obj->queue_cv->send;
148 0 0       0 if ($h{on_error}) {
149 0         0 $h{on_error}->($obj, $error);
150             }
151             else {
152 0         0 warn "Error occured: $error";
153             }
154             # the handle::udp self destroys right after calling the on_error_handler
155             # so the client have to do the same
156 0 0       0 $obj->destroy() if $fatal;
157 1         17 };
158              
159 1         28 $obj->handler( AnyEvent::Handle::UDP->new(%udp_handle_args) );
160              
161 1         6114 $obj->init();
162              
163 1         19 return $obj;
164             }
165              
166             sub clear_send_cache {
167 0     0 0 0 my $self = shift;
168 0         0 my $send_cache = $self->send_cache();
169 0         0 $self->send_cache({});
170 0 0       0 if ($send_cache) {
171 0         0 my @ordered_reqids = sort { $send_cache->{$a}{time_cached} <=> $send_cache->{$b}{time_cached} } keys %$send_cache;
  0         0  
172 0         0 foreach my $request_id (@ordered_reqids) {
173 0 0       0 if (my $cb = $send_cache->{$request_id}{callback}) {
174 0         0 $cb->();
175             }
176             }
177             }
178             }
179              
180             sub _send_packet {
181 2     2   4 my ($self, $packet) = @_;
182              
183 2         35 $self->queue_cnt($self->queue_cnt() + 1);
184              
185             # +1
186 2         76 $self->queue_cv()->begin;
187 2         43 $self->write_cv()->begin;
188 2         36 $self->read_cv()->begin;
189              
190 2         41 my $cv = AnyEvent->condvar;
191              
192             $cv->cb(sub {
193 2     2   298 $self->sent_cnt($self->sent_cnt() + 1);
194             # -1
195 2         40 $self->write_cv()->end;
196 2         42 });
197              
198             # cv->send is called by Handle::UDP when packet is sent
199 2         49 $self->handler()->push_send($packet, undef, $cv);
200             }
201              
202             # wait for Handle to send all queued packets (or timeout)
203             # object is not usable after it - call init()
204             sub wait {
205 1     1 1 8 my $self = shift;
206              
207 1         16 $self->queue_cv()->recv();
208             }
209              
210             # reset vars - need to be called after wait() or on_ready()
211             sub init {
212 1     1 0 3 my $self = shift;
213              
214 1         27 $self->read_cv(AnyEvent->condvar);
215 1         57 $self->write_cv(AnyEvent->condvar);
216 1         49 $self->queue_cv(AnyEvent->condvar);
217 1         57 $self->sent_cnt(0);
218 1         42 $self->reply_cnt(0);
219 1         32 $self->queue_cnt(0);
220 1         37 $self->send_cache({});
221 1         23 $self->last_request_id( int rand(0x100) );
222             }
223              
224             # close open socket, object is unusable after it was called
225             sub destroy {
226 0     0 1 0 my $self = shift;
227 0         0 $self->handler()->destroy();
228 0         0 $self->handler(undef);
229             }
230              
231             my $_IN_GLOBAL_DESTRUCTION = 0;
232             END {
233 1     1   2101 $_IN_GLOBAL_DESTRUCTION = 1;
234             }
235              
236             sub DESTROY {
237 0     0   0 my $self = shift;
238 0 0       0 if (defined ${^GLOBAL_PHASE}) {
239             # >= 5.14
240 0 0       0 return if (${^GLOBAL_PHASE} eq 'DESTRUCT');
241             }
242             else {
243             # before 5.14, see also Devel::GlobalDestruction
244 0 0       0 return if $_IN_GLOBAL_DESTRUCTION;
245             }
246              
247 0 0       0 return if (! $self->handler());
248 0         0 $self->handler()->destroy();
249             }
250              
251             # group wait
252             # cv is AnyEvent condition var passed outside
253             #
254             # Example:
255             # my $cv = AnyEvent->condvar;
256             # $nas1->on_ready($cv);
257             # $nas2->on_ready($cv);
258             # $nas3->on_ready($cv);
259             # $cv->recv;
260             #
261             sub on_ready {
262 0     0 1 0 my ($self, $cv) = @_;
263              
264 0         0 $cv->begin();
265 0     0   0 $self->queue_cv()->cb(sub { $cv->end });
  0         0  
266             }
267              
268             sub load_dictionary {
269 0     0 1 0 my ($class, $path) = @_;
270 0         0 my $dict = Data::Radius::Dictionary->load_file($path);
271              
272 0 0       0 if(ref($class)) {
273 0         0 $class->packer()->dict($dict);
274             }
275              
276 0         0 return $dict;
277             }
278              
279             sub new_request_id {
280 2     2 0 2 my $self = shift;
281 2         29 my $last_request_id = $self->last_request_id();
282 2         10 my $new_request_id = ($last_request_id + 1) & 0xFF;
283 2         28 my $send_cache = $self->send_cache();
284 2         12 while (exists $send_cache->{$new_request_id}) {
285 0         0 $new_request_id = ($new_request_id + 1) & 0xFF;
286 0 0       0 return undef if $new_request_id == $last_request_id; # send cache full ??
287             }
288 2         5 return $new_request_id;
289             }
290              
291             # add packet to the queue
292             # type - radius request packet type code or its text alias
293             # av_list - list of attributes in {Name => ... Value => ... } form
294             # cb - optional callback to be called on result:
295             # - when received response as $cb->($resp_type, $resp_av_list)
296             # - when failed (eg time out, invalid or non matching response)
297             # with empty parameter list cb->();
298             sub send_packet {
299 2     2 1 12 my ($self, $type, $av_list, $cb) = @_;
300              
301 2 50       39 if ($self->queue_cnt >= MAX_QUEUE) {
302             # queue overflow
303 0         0 return undef;
304             }
305              
306 2         16 my $request_id = $self->new_request_id();
307 2 50       5 return undef if !defined $request_id;
308              
309 2 50       9 $type = $RADIUS_PACKET_TYPES{$type} if exists $RADIUS_PACKET_TYPES{$type};
310              
311 2         30 my ($packet, $req_id, $auth) = $self->packer()->build(
312             type => $type,
313             av_list => $av_list,
314             with_msg_auth => 1,
315             request_id => $request_id,
316             );
317              
318 2         1119 $self->last_request_id( $req_id );
319              
320             # required to verify reply
321 2         57 $self->send_cache()->{ $req_id } = {
322             authenticator => $auth,
323             type => $type,
324             callback => $cb,
325             time_cached => AE::now(),
326             };
327              
328 2         32 $self->_send_packet($packet);
329              
330 2 50       99 return wantarray() ? ($req_id, $auth) : $req_id;
331             }
332              
333             # shortcut methods:
334              
335             sub send_auth {
336 2     2 1 62 my $self = shift;
337 2         6 return $self->send_packet(AUTH => @_);
338             }
339              
340             sub send_acct {
341 0     0 1   my $self = shift;
342 0           return $self->send_packet(ACCT => @_);
343             }
344              
345             sub send_pod {
346 0     0 1   my $self = shift;
347 0           return $self->send_packet(POD => @_);
348             }
349              
350             sub send_coa {
351 0     0 1   my $self = shift;
352 0           return $self->send_packet(COA => @_);
353             }
354              
355             1;
356              
357             __END__