File Coverage

blib/lib/AnyEvent/Radius/Client.pm
Criterion Covered Total %
statement 85 145 58.6
branch 10 46 21.7
condition 6 15 40.0
subroutine 18 30 60.0
pod 10 12 83.3
total 129 248 52.0


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