File Coverage

blib/lib/POE/Component/Client/DNS/Recursive.pm
Criterion Covered Total %
statement 193 257 75.1
branch 46 78 58.9
condition 12 25 48.0
subroutine 21 22 95.4
pod 1 1 100.0
total 273 383 71.2


line stmt bran cond sub pod time code
1             package POE::Component::Client::DNS::Recursive;
2             $POE::Component::Client::DNS::Recursive::VERSION = '1.14';
3             #ABSTRACT: A recursive DNS client for POE
4              
5 7     7   1554067 use strict;
  7         65  
  7         216  
6 7     7   41 use warnings;
  7         18  
  7         213  
7 7     7   38 use Carp;
  7         15  
  7         510  
8 7     7   81 use Socket qw[:all];
  7         22  
  7         8586  
9 7     7   3497 use Net::IP::Minimal qw(:PROC);
  7         6071  
  7         811  
10 7     7   3254 use IO::Socket::IP;
  7         31994  
  7         59  
11 7     7   6184 use POE qw(NFA);
  7         17  
  7         92  
12 7     7   48320 use Net::DNS::Packet;
  7         184084  
  7         19740  
13              
14             my @hc_hints = qw(
15             198.41.0.4
16             192.228.79.201
17             192.33.4.12
18             128.8.10.90
19             192.203.230.10
20             192.5.5.241
21             192.112.36.4
22             128.63.2.53
23             192.36.148.17
24             );
25              
26             sub resolve {
27 6     6 1 6117 my $package = shift;
28 6         29 my %opts = @_;
29 6         57 $opts{lc $_} = delete $opts{$_} for keys %opts;
30             croak "$package requires a 'host' argument\n"
31 6 50       31 unless $opts{host};
32             croak "$package requires an 'event' argument\n"
33 6 50       32 unless $opts{event};
34 6 100 66     61 $opts{nameservers} = [ ] unless $opts{nameservers} and ref $opts{nameservers} eq 'ARRAY';
35 6         24 @{ $opts{nameservers} } = grep { ip_get_version( $_ ) } @{ $opts{nameservers} };
  6         77  
  1         8  
  6         19  
36 6         28 my $options = delete $opts{options};
37 6         50 my $self = bless \%opts, $package;
38 6         36 my $sender = $poe_kernel->get_active_session();
39 6         55 $self->{_sender} = $sender;
40 6         125 POE::NFA->spawn(
41             object_states => {
42             initial => [
43             $self => { setup => '_start' },
44             $self => [qw(_default)],
45             ],
46             hints => [
47             $self => {
48             _init => '_hints_go',
49             _setup => '_send',
50             _read => '_hints',
51             _timeout => '_hints_timeout',
52             },
53             ],
54             query => [
55             $self => {
56             _setup => '_send',
57             _read => '_query',
58             _timeout => '_query_timeout',
59             },
60             ],
61             done => [
62             $self => [qw(_close _error)],
63             ],
64             },
65             runstate => $self,
66             )->goto_state( 'initial' => 'setup' );
67 6         207 return $self;
68             }
69              
70             sub _default {
71 6     6   3384 return 0;
72             }
73              
74             sub _start {
75 6     6   8395 my ($kernel,$machine,$runstate) = @_[KERNEL,MACHINE,RUNSTATE];
76 6         19 my $sender = $runstate->{_sender};
77 6 50 33     45 if ( $kernel == $sender and !$runstate->{session} ) {
78 0         0 croak "Not called from another POE session and 'session' wasn't set\n";
79             }
80 6         17 my $sender_id;
81 6 50       25 if ( $runstate->{session} ) {
82 0 0       0 if ( my $ref = $kernel->alias_resolve( $runstate->{session} ) ) {
83 0         0 $sender_id = $ref->ID();
84             }
85             else {
86 0         0 croak "Could not resolve 'session' to a valid POE session\n";
87             }
88             }
89             else {
90 6         41 $sender_id = $sender->ID();
91             }
92             $kernel->refcount_increment( $sender_id, __PACKAGE__ )
93 6 100       112 unless ref $runstate->{event} eq 'POE::Session::AnonEvent';
94 6         204 $kernel->detach_myself();
95 6         489 $runstate->{sender_id} = $sender_id;
96 6   33     67 my $type = $runstate->{type} || ( ip_get_version( $runstate->{host} ) ? 'PTR' : 'A' );
97 6   50     229 my $class = $runstate->{class} || 'IN';
98 6         21 $runstate->{qstack} = [ ];
99             $runstate->{current} = {
100             query => $runstate->{host},
101             type => $type,
102 6         92 packet => Net::DNS::Packet->new($runstate->{host},$type,$class),
103             };
104 6         1137 $runstate->{socket} = IO::Socket::IP->new( Proto => 'udp' );
105 6         3965 $machine->goto_state( 'hints', '_init' );
106 6         620 return;
107             }
108              
109             sub _hints_go {
110 7     7   3036 my ($kernel,$machine,$runstate) = @_[KERNEL,MACHINE,RUNSTATE];
111 7         16 my $hints;
112 7 100       17 if ( scalar @{ $runstate->{nameservers} } ) {
  7         28  
113 1         3 $hints = $runstate->{nameservers};
114             }
115             else {
116 6         45 $hints = [ @hc_hints ];
117             }
118 7         18 $runstate->{_hints} = $hints;
119 7         51 $machine->goto_state( 'hints', '_setup', Net::DNS::Packet->new('.','NS','IN'), splice( @$hints, rand($#{$hints}), 1) );
  7         1228  
120 7         698 return;
121             }
122              
123             sub _send {
124 26     26   12229 my ($machine,$runstate,$state,$packet,$ns) = @_[MACHINE,RUNSTATE,STATE,ARG0,ARG1];
125 26         79 my $socket = $runstate->{socket};
126 26         119 my $data = $packet->data;
127 26         84824 my $ai;
128             {
129 26         48 my %hints = (flags => AI_NUMERICHOST, socktype => SOCK_DGRAM, protocol => IPPROTO_UDP);
  26         126  
130 26         356 my ($err, @res) = getaddrinfo($ns, '53', \%hints);
131 26 50       112 if ( $err ) {
132 0         0 warn "'$ns' didn't produce an valid server address\n";
133 0         0 $machine->goto_state( 'done', '_error', $err );
134 0         0 return;
135             }
136 26         88 $ai = shift @res;
137             }
138 26         179 $socket->socket( $ai->{family}, $ai->{socktype}, $ai->{protocol} );
139 26 50       5744 unless ( send( $socket, $data, 0, $ai->{addr} ) == length($data) ) {
140 0         0 $machine->goto_state( 'done', '_error', "$ns: $!" );
141 0         0 return;
142             }
143 26         232 $poe_kernel->select_read( $socket, '_read' );
144 26   50     3280 $poe_kernel->delay( '_timeout', $runstate->{timeout} || 5 );
145 26         3090 return;
146             }
147              
148             sub _hints {
149 7     7   85120 my ($machine,$runstate,$socket) = @_[MACHINE,RUNSTATE,ARG0];
150 7         42 $poe_kernel->delay( '_timeout' );
151 7         1020 my $packet = _read_socket( $socket );
152 7         19 my %hints;
153 7 100       42 if (my @ans = $packet->answer) {
154 6         98 foreach my $rr (@ans) {
155 78 50 33     251 if ($rr->name =~ /^\.?$/ and
156             $rr->type eq "NS") {
157             # Found root authority
158 78         3510 my $server = lc $rr->rdstring;
159 78         8098 $server =~ s/\.$//;
160 78         338 $hints{$server} = [];
161             }
162             }
163 6         65 foreach my $rr ($packet->additional) {
164 88 50       4219 if (my $server = lc $rr->name){
165 88 100       4447 if ( $rr->type eq "A") {
166 72 50       909 if ($hints{$server}) {
167 72         123 push @{ $hints{$server} }, $rr->rdstring;
  72         216  
168             }
169             }
170             }
171             }
172             }
173 7 100       231 if ( $runstate->{trace} ) {
174 2 100       14 if ( ref $runstate->{trace} eq 'POE::Session::AnonEvent' ) {
175 1         9 $runstate->{trace}->( $packet );
176             }
177             else {
178 1         8 $poe_kernel->post( $runstate->{sender_id}, $runstate->{trace}, $packet );
179             }
180             }
181 7         356 $runstate->{hints} = \%hints;
182 7         39 my @ns = _ns_from_cache( $runstate->{hints} );
183 7 100       54 unless ( scalar @ns ) {
184 1         4 $machine->goto_state( 'hints', '_init' );
185 1         113 return;
186             }
187 6         23 my $query = $runstate->{current};
188 6         29 $query->{servers} = \@ns;
189 6         45 my ($nameserver) = splice @ns, rand($#ns), 1;
190 6         74 $machine->goto_state( 'query', '_setup', $query->{packet}, $nameserver );
191 6         927 return;
192             }
193              
194             sub _hints_timeout {
195 3     3   15015958 my ($machine,$runstate) = @_[MACHINE,RUNSTATE];
196 3         20 my $hints = $runstate->{_hints};
197 3 50       15 if ( scalar @{ $hints } ) {
  3 0       27  
198 3         67 $machine->goto_state( 'hints', '_setup', Net::DNS::Packet->new('.','NS','IN'), splice( @$hints, rand($#{$hints}), 1) );
  3         458  
199             }
200             elsif ( defined $runstate->{nameservers} ) {
201 0         0 $machine->goto_state( 'hints', '_init' );
202 0         0 return;
203             }
204             else {
205 0         0 $machine->goto_state( 'done', '_error', 'Ran out of authority records' );
206             }
207 3         601 return;
208             }
209              
210             sub _query {
211 16     16   348368 my ($machine,$runstate,$socket) = @_[MACHINE,RUNSTATE,ARG0];
212 16         127 $poe_kernel->delay( '_timeout' );
213 16         2044 my $packet = _read_socket( $socket );
214 16         53 my @ns;
215 16         78 my $status = $packet->header->rcode;
216 16 100       2893 if ( $status ne 'NOERROR' ) {
217 1         5 $machine->goto_state( 'done', '_error', $status );
218 1         113 return;
219             }
220 15 100       71 if (my @ans = $packet->answer) {
221             # This is the end of the chain.
222 5 50       58 unless ( scalar @{ $runstate->{qstack} } ) {
  5         36  
223 5         78 $machine->goto_state( 'done', '_close', $packet );
224 5         726 return;
225             }
226             # Okay we have queries pending.
227 0         0 push @ns, $_->rdstring for grep { $_->type eq 'A' } @ans;
  0         0  
228 0         0 $runstate->{current} = pop @{ $runstate->{qstack} };
  0         0  
229             }
230             else {
231 10 100       171 if ( $runstate->{trace} ) {
232 4         29 $poe_kernel->post( $runstate->{sender_id}, $runstate->{trace}, $packet );
233             }
234 10         633 my $authority = _authority( $packet );
235 10         43 @ns = _ns_from_cache( $authority );
236 10 50       78 unless ( scalar @ns ) {
237 0         0 $runstate->{current}->{authority} = $authority;
238 0         0 push @{ $runstate->{qstack} }, $runstate->{current};
  0         0  
239 0         0 my $host = ( keys %{ $authority } )[rand scalar keys %{ $authority }];
  0         0  
  0         0  
240 0         0 delete $authority->{$host};
241             $runstate->{current} = {
242 0         0 query => $host,
243             type => 'A',
244             packet => Net::DNS::Packet->new($host,'A','IN'),
245             };
246 0         0 @ns = _ns_from_cache( $runstate->{hints} );
247             }
248             }
249 10         41 my $query = $runstate->{current};
250 10         113 $query->{servers} = \@ns;
251 10         56 my ($nameserver) = splice @ns, rand($#ns), 1;
252 10         77 $poe_kernel->yield( '_setup', $query->{packet}, $nameserver );
253 10         1096 return;
254             }
255              
256             sub _query_timeout {
257 0     0   0 my ($machine,$runstate) = @_[MACHINE,RUNSTATE];
258 0         0 my $query = $runstate->{current};
259 0         0 my $servers = $query->{servers};
260 0         0 my ($nameserver) = splice @{ $servers }, rand($#{ $servers }), 1;
  0         0  
  0         0  
261             # actually check here if there is something on the stack.
262             # pop off the most recent, and get the next authority record
263             # push back on to the stack and do a lookup for the authority
264             # record. No authority records left, then complain and bailout.
265 0 0       0 unless ( $nameserver ) {
266 0 0       0 if ( scalar @{ $runstate->{qstack} } ) {
  0         0  
267 0         0 $runstate->{current} = pop @{ $runstate->{qstack} };
  0         0  
268 0         0 my $host = ( keys %{ $runstate->{current}->{authority} } )[rand scalar keys %{ $runstate->{current}->{authority} }];
  0         0  
  0         0  
269 0 0       0 unless ( $host ) { # Oops
270 0         0 $machine->goto_state( 'done', '_error', 'Ran out of authority records' );
271 0         0 return; # OMG
272             }
273 0         0 delete $runstate->{current}->{authority}->{ $host };
274 0         0 push @{ $runstate->{qstack} }, $runstate->{current};
  0         0  
275             $runstate->{current} = {
276 0         0 query => $host,
277             type => 'A',
278             packet => Net::DNS::Packet->new($host,'A','IN'),
279             };
280 0         0 my @ns = _ns_from_cache( $runstate->{hints} );
281 0         0 $runstate->{current}->{servers} = \@ns;
282 0         0 ($nameserver) = splice @ns, rand($#ns), 1;
283             }
284             else {
285 0         0 $machine->goto_state( 'done', '_error', 'Ran out of authority records' );
286 0         0 return; # OMG
287             }
288             }
289 0 0       0 unless ( $nameserver ) { # SERVFAIL? maybe
290 0         0 $machine->goto_state( 'done', '_error', 'Ran out of nameservers to query' );
291 0         0 return;
292             }
293 0         0 $poe_kernel->yield( '_setup', $query->{packet}, $nameserver );
294 0         0 return;
295             }
296              
297             sub _error {
298 1     1   167 my ($kernel,$machine,$runstate,$error) = @_[KERNEL,MACHINE,RUNSTATE,ARG0];
299 1         5 $kernel->select_read( $runstate->{socket} ); # Just in case
300 1         24 my $resp = {};
301 1         7 $resp->{$_} = $runstate->{$_} for qw(host type class context);
302 1         3 $resp->{response} = undef;
303 1         3 $resp->{error} = $error;
304 1         2 delete $runstate->{trace};
305 1 50       4 if ( ref $runstate->{event} eq 'POE::Session::AnonEvent' ) {
306 0         0 my $postback = delete $runstate->{event};
307 0         0 $postback->( $resp );
308             }
309             else {
310 1         4 $kernel->post( $runstate->{sender_id}, $runstate->{event}, $resp );
311 1         115 $kernel->refcount_decrement( $runstate->{sender_id}, __PACKAGE__ );
312             }
313 1         50 return;
314             }
315              
316             sub _close {
317 5     5   1106 my ($kernel,$machine,$runstate,$packet) = @_[KERNEL,MACHINE,RUNSTATE,ARG0];
318 5         33 $kernel->select_read( $runstate->{socket} ); # Just in case
319 5         216 my $resp = {};
320 5         77 $resp->{$_} = $runstate->{$_} for qw(host type class context);
321 5         23 $resp->{response} = $packet;
322 5         22 delete $runstate->{trace};
323 5 100       143 if ( ref $runstate->{event} eq 'POE::Session::AnonEvent' ) {
324 2         10 my $postback = delete $runstate->{event};
325 2         13 $postback->( $resp );
326             }
327             else {
328 3         26 $kernel->post( $runstate->{sender_id}, $runstate->{event}, $resp );
329 3         399 $kernel->refcount_decrement( $runstate->{sender_id}, __PACKAGE__ );
330             }
331 5         737 return;
332             }
333              
334             sub _authority {
335 10   50 10   45 my $packet = shift || return;
336 10         26 my %hints;
337 10 50       61 if (my @ans = $packet->authority) {
338 10         132 foreach my $rr (@ans) {
339 85 50       269 if ( $rr->type eq 'NS') {
340             # Found root authority
341 85         1140 my $server = lc $rr->rdstring;
342 85         8616 $server =~ s/\.$//;
343 85         392 $hints{$server} = [];
344             }
345             }
346 10         87 foreach my $rr ($packet->additional) {
347 101 50       3639 if (my $server = lc $rr->name){
348 101 50 66     4852 push @{ $hints{$server} }, $rr->rdstring if $rr->type eq 'A' and $hints{$server};
  64         957  
349             }
350             }
351             }
352 10         513 return \%hints;
353             }
354              
355             sub _read_socket {
356 23   50 23   124 my $socket = shift || return;
357 23         124 $poe_kernel->select_read( $socket );
358 23         3462 my $message;
359 23 50       219 unless ( $socket->recv( $message, 512 ) ) {
360 0         0 warn "$!\n";
361 0         0 return;
362             }
363 23         1242 my ($in, $len) = Net::DNS::Packet->new( \$message, 0 );
364 23 50       52358 if ( $@ ) {
365 0         0 warn "$@\n";
366 0         0 return;
367             }
368 23 50       97 unless ( $len ) {
369 0         0 warn "Bad size\n";
370 0         0 return;
371             }
372 23         75 return $in;
373             }
374              
375             sub _ns_from_cache {
376 17   50 17   74 my $hashref = shift || return;
377 17         53 my @ns;
378 17         70 foreach my $ns (keys %{ $hashref }) {
  17         99  
379 163 100       271 push @ns, @{ $hashref->{$ns} } if scalar @{ $hashref->{$ns} };
  136         279  
  163         375  
380             }
381 17         90 return @ns;
382             }
383              
384             'Recursive lookup, recursive lookup, recursive lookup ....';
385              
386             __END__