File Coverage

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


line stmt bran cond sub pod time code
1             package POE::Component::Client::DNS::Recursive;
2             $POE::Component::Client::DNS::Recursive::VERSION = '1.10';
3             #ABSTRACT: A recursive DNS client for POE
4              
5 6     6   612200 use strict;
  6         10  
  6         159  
6 6     6   25 use warnings;
  6         6  
  6         155  
7 6     6   19 use Carp;
  6         8  
  6         332  
8 6     6   27 use Socket qw[:all];
  6         9  
  6         6291  
9 6     6   2795 use Net::IP::Minimal qw(:PROC);
  6         3539  
  6         633  
10 6     6   3058 use IO::Socket::IP;
  6         23235  
  6         29  
11 6     6   4887 use POE qw(NFA);
  6         7  
  6         91  
12 6     6   27264 use Net::DNS::Packet;
  6         134041  
  6         11400  
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 2844 my $package = shift;
28 6         24 my %opts = @_;
29 6         44 $opts{lc $_} = delete $opts{$_} for keys %opts;
30             croak "$package requires a 'host' argument\n"
31 6 50       24 unless $opts{host};
32             croak "$package requires an 'event' argument\n"
33 6 50       19 unless $opts{event};
34 6 100 66     46 $opts{nameservers} = [ ] unless $opts{nameservers} and ref $opts{nameservers} eq 'ARRAY';
35 6         11 @{ $opts{nameservers} } = grep { ip_get_version( $_ ) } @{ $opts{nameservers} };
  6         30  
  2         38  
  6         18  
36 6         15 my $options = delete $opts{options};
37 6         20 my $self = bless \%opts, $package;
38 6         41 my $sender = $poe_kernel->get_active_session();
39 6         48 $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         208 return $self;
68             }
69              
70             sub _default {
71 6     6   2187 return 0;
72             }
73              
74             sub _start {
75 6     6   5718 my ($kernel,$machine,$runstate) = @_[KERNEL,MACHINE,RUNSTATE];
76 6         15 my $sender = $runstate->{_sender};
77 6 50 33     32 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         10 my $sender_id;
81 6 50       20 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         30 $sender_id = $sender->ID();
91             }
92             $kernel->refcount_increment( $sender_id, __PACKAGE__ )
93 6 100       57 unless ref $runstate->{event} eq 'POE::Session::AnonEvent';
94 6         129 $kernel->detach_myself();
95 6         261 $runstate->{sender_id} = $sender_id;
96 6   33     55 my $type = $runstate->{type} || ( ip_get_version( $runstate->{host} ) ? 'PTR' : 'A' );
97 6   50     144 my $class = $runstate->{class} || 'IN';
98 6         16 $runstate->{qstack} = [ ];
99             $runstate->{current} = {
100             query => $runstate->{host},
101             type => $type,
102 6         68 packet => Net::DNS::Packet->new($runstate->{host},$type,$class),
103             };
104 6         820 $runstate->{socket} = IO::Socket::IP->new( Proto => 'udp' );
105 6         3580 $machine->goto_state( 'hints', '_init' );
106 6         379 return;
107             }
108              
109             sub _hints_go {
110 8     8   2029 my ($kernel,$machine,$runstate) = @_[KERNEL,MACHINE,RUNSTATE];
111 8         12 my $hints;
112 8 100       9 if ( scalar @{ $runstate->{nameservers} } ) {
  8         25  
113 2         3 $hints = $runstate->{nameservers};
114             }
115             else {
116 6         27 $hints = [ @hc_hints ];
117             }
118 8         19 $runstate->{_hints} = $hints;
119 8         43 $machine->goto_state( 'hints', '_setup', Net::DNS::Packet->new('.','NS','IN'), splice( @$hints, rand($#{$hints}), 1) );
  8         568  
120 8         461 return;
121             }
122              
123             sub _send {
124 25     25   8212 my ($machine,$runstate,$state,$packet,$ns) = @_[MACHINE,RUNSTATE,STATE,ARG0,ARG1];
125 25         53 my $socket = $runstate->{socket};
126 25         101 my $data = $packet->data;
127 25         15059 my $ai;
128             {
129 25         32 my %hints = (flags => AI_NUMERICHOST, socktype => SOCK_DGRAM, protocol => IPPROTO_UDP);
  25         115  
130 25         326 my ($err, @res) = getaddrinfo($ns, '53', \%hints);
131 25 50       80 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 25         60 $ai = shift @res;
137             }
138 25         146 $socket->socket( $ai->{family}, $ai->{socktype}, $ai->{protocol} );
139 25 50       4261 unless ( send( $socket, $data, 0, $ai->{addr} ) == length($data) ) {
140 0         0 $machine->goto_state( 'done', '_error', "$ns: $!" );
141 0         0 return;
142             }
143 25         141 $poe_kernel->select_read( $socket, '_read' );
144 25   50     2294 $poe_kernel->delay( '_timeout', $runstate->{timeout} || 5 );
145 25         1968 return;
146             }
147              
148             sub _hints {
149 8     8   193638 my ($machine,$runstate,$socket) = @_[MACHINE,RUNSTATE,ARG0];
150 8         36 $poe_kernel->delay( '_timeout' );
151 8         775 my $packet = _read_socket( $socket );
152 8         15 my %hints;
153 8 50       36 if (my @ans = $packet->answer) {
154 8         107 foreach my $rr (@ans) {
155 104 50 33     228 if ($rr->name =~ /^\.?$/ and
156             $rr->type eq "NS") {
157             # Found root authority
158 104         2971 my $server = lc $rr->rdatastr;
159 104         4583 $server =~ s/\.$//;
160 104         341 $hints{$server} = [];
161             }
162             }
163 8         50 foreach my $rr ($packet->additional) {
164 86 50       1639 if (my $server = lc $rr->name){
165 86 100       2927 if ( $rr->type eq "A") {
166 66 50       496 if ($hints{$server}) {
167 66         45 push @{ $hints{$server} }, $rr->rdatastr;
  66         185  
168             }
169             }
170             }
171             }
172             }
173 8 100       109 if ( $runstate->{trace} ) {
174 2 100       10 if ( ref $runstate->{trace} eq 'POE::Session::AnonEvent' ) {
175 1         7 $runstate->{trace}->( $packet );
176             }
177             else {
178 1         9 $poe_kernel->post( $runstate->{sender_id}, $runstate->{trace}, $packet );
179             }
180             }
181 8         368 $runstate->{hints} = \%hints;
182 8         35 my @ns = _ns_from_cache( $runstate->{hints} );
183 8 100       28 unless ( scalar @ns ) {
184 2         6 $machine->goto_state( 'hints', '_init' );
185 2         152 return;
186             }
187 6         13 my $query = $runstate->{current};
188 6         16 $query->{servers} = \@ns;
189 6         31 my ($nameserver) = splice @ns, rand($#ns), 1;
190 6         38 $machine->goto_state( 'query', '_setup', $query->{packet}, $nameserver );
191 6         757 return;
192             }
193              
194             sub _hints_timeout {
195 1     1   5002272 my ($machine,$runstate) = @_[MACHINE,RUNSTATE];
196 1         3 my $hints = $runstate->{_hints};
197 1 50       3 if ( scalar @{ $hints } ) {
  1 0       4  
198 1         13 $machine->goto_state( 'hints', '_setup', Net::DNS::Packet->new('.','NS','IN'), splice( @$hints, rand($#{$hints}), 1) );
  1         80  
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 1         94 return;
208             }
209              
210             sub _query {
211 16     16   788151 my ($machine,$runstate,$socket) = @_[MACHINE,RUNSTATE,ARG0];
212 16         88 $poe_kernel->delay( '_timeout' );
213 16         1844 my $packet = _read_socket( $socket );
214 16         28 my @ns;
215 16         72 my $status = $packet->header->rcode;
216 16 100       1869 if ( $status ne 'NOERROR' ) {
217 1         5 $machine->goto_state( 'done', '_error', $status );
218 1         147 return;
219             }
220 15 100       78 if (my @ans = $packet->answer) {
221             # This is the end of the chain.
222 5 50       55 unless ( scalar @{ $runstate->{qstack} } ) {
  5         28  
223 5         35 $machine->goto_state( 'done', '_close', $packet );
224 5         529 return;
225             }
226             # Okay we have queries pending.
227 0         0 push @ns, $_->rdatastr for grep { $_->type eq 'A' } @ans;
  0         0  
228 0         0 $runstate->{current} = pop @{ $runstate->{qstack} };
  0         0  
229             }
230             else {
231 10 100       112 if ( $runstate->{trace} ) {
232 4         27 $poe_kernel->post( $runstate->{sender_id}, $runstate->{trace}, $packet );
233             }
234 10         470 my $authority = _authority( $packet );
235 10         42 @ns = _ns_from_cache( $authority );
236 10 50       55 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         24 my $query = $runstate->{current};
250 10         24 $query->{servers} = \@ns;
251 10         59 my ($nameserver) = splice @ns, rand($#ns), 1;
252 10         66 $poe_kernel->yield( '_setup', $query->{packet}, $nameserver );
253 10         780 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   149 my ($kernel,$machine,$runstate,$error) = @_[KERNEL,MACHINE,RUNSTATE,ARG0];
299 1         5 $kernel->select_read( $runstate->{socket} ); # Just in case
300 1         23 my $resp = {};
301 1         8 $resp->{$_} = $runstate->{$_} for qw(host type class context);
302 1         3 $resp->{response} = undef;
303 1         3 $resp->{error} = $error;
304 1         3 delete $runstate->{trace};
305 1 50       6 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         7 $kernel->post( $runstate->{sender_id}, $runstate->{event}, $resp );
311 1         89 $kernel->refcount_decrement( $runstate->{sender_id}, __PACKAGE__ );
312             }
313 1         52 return;
314             }
315              
316             sub _close {
317 5     5   632 my ($kernel,$machine,$runstate,$packet) = @_[KERNEL,MACHINE,RUNSTATE,ARG0];
318 5         20 $kernel->select_read( $runstate->{socket} ); # Just in case
319 5         84 my $resp = {};
320 5         44 $resp->{$_} = $runstate->{$_} for qw(host type class context);
321 5         11 $resp->{response} = $packet;
322 5         18 delete $runstate->{trace};
323 5 100       73 if ( ref $runstate->{event} eq 'POE::Session::AnonEvent' ) {
324 2         5 my $postback = delete $runstate->{event};
325 2         11 $postback->( $resp );
326             }
327             else {
328 3         10 $kernel->post( $runstate->{sender_id}, $runstate->{event}, $resp );
329 3         241 $kernel->refcount_decrement( $runstate->{sender_id}, __PACKAGE__ );
330             }
331 5         398 return;
332             }
333              
334             sub _authority {
335 10   50 10   37 my $packet = shift || return;
336 10         12 my %hints;
337 10 50       38 if (my @ans = $packet->authority) {
338 10         120 foreach my $rr (@ans) {
339 85 50       184 if ( $rr->type eq 'NS') {
340             # Found root authority
341 85         742 my $server = lc $rr->rdatastr;
342 85         4156 $server =~ s/\.$//;
343 85         277 $hints{$server} = [];
344             }
345             }
346 10         73 foreach my $rr ($packet->additional) {
347 90 50       1889 if (my $server = lc $rr->name){
348 90 50 66     2977 push @{ $hints{$server} }, $rr->rdatastr if $rr->type eq 'A' and $hints{$server};
  85         836  
349             }
350             }
351             }
352 10         158 return \%hints;
353             }
354              
355             sub _read_socket {
356 24   50 24   151 my $socket = shift || return;
357 24         104 $poe_kernel->select_read( $socket );
358 24         2372 my $message;
359 24 50       217 unless ( $socket->recv( $message, 512 ) ) {
360 0         0 warn "$!\n";
361 0         0 return;
362             }
363 24         882 my ($in, $len) = Net::DNS::Packet->new( \$message, 0 );
364 24 50       49679 if ( $@ ) {
365 0         0 warn "$@\n";
366 0         0 return;
367             }
368 24 50       71 unless ( $len ) {
369 0         0 warn "Bad size\n";
370 0         0 return;
371             }
372 24         59 return $in;
373             }
374              
375             sub _ns_from_cache {
376 18   50 18   65 my $hashref = shift || return;
377 18         23 my @ns;
378 18         24 foreach my $ns (keys %{ $hashref }) {
  18         113  
379 189 100       129 push @ns, @{ $hashref->{$ns} } if scalar @{ $hashref->{$ns} };
  151         214  
  189         326  
380             }
381 18         76 return @ns;
382             }
383              
384             'Recursive lookup, recursive lookup, recursive lookup ....';
385              
386             __END__