File Coverage

blib/lib/AnyEvent/Radius/Client.pm
Criterion Covered Total %
statement 105 168 62.5
branch 13 56 23.2
condition 5 12 41.6
subroutine 22 33 66.6
pod 11 13 84.6
total 156 282 55.3


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