File Coverage

blib/lib/POE/Component/Client/DNS.pm
Criterion Covered Total %
statement 207 242 85.5
branch 73 94 77.6
condition 24 42 57.1
subroutine 28 31 90.3
pod 4 4 100.0
total 336 413 81.3


line stmt bran cond sub pod time code
1             # License and documentation are after __END__.
2             # vim: ts=2 sw=2 expandtab
3              
4             package POE::Component::Client::DNS;
5             $POE::Component::Client::DNS::VERSION = '1.054';
6 6     6   1047834 use strict;
  6         19  
  6         183  
7              
8 6     6   32 use Carp qw(croak);
  6         11  
  6         294  
9              
10 6     6   30 use Socket qw(unpack_sockaddr_in inet_ntoa);
  6         12  
  6         321  
11 6     6   4677 use Net::DNS;
  6         390342  
  6         674  
12 6     6   62 use POE;
  6         12  
  6         74  
13              
14 6     6   2052 use constant DEBUG => 0;
  6         13  
  6         371  
15              
16             # A hosts file we found somewhere.
17              
18             my $global_hosts_file;
19              
20             # Object fields. "SF" stands for "self".
21              
22 6     6   61 use constant SF_ALIAS => 0;
  6         13  
  6         281  
23 6     6   30 use constant SF_TIMEOUT => 1;
  6         10  
  6         284  
24 6     6   28 use constant SF_NAMESERVERS => 2;
  6         12  
  6         259  
25 6     6   89 use constant SF_RESOLVER => 3;
  6         17  
  6         290  
26 6     6   29 use constant SF_HOSTS_FILE => 4;
  6         10  
  6         271  
27 6     6   26 use constant SF_HOSTS_MTIME => 5;
  6         19  
  6         298  
28 6     6   26 use constant SF_HOSTS_CTIME => 6;
  6         11  
  6         288  
29 6     6   30 use constant SF_HOSTS_INODE => 7;
  6         12  
  6         264  
30 6     6   29 use constant SF_HOSTS_CACHE => 8;
  6         10  
  6         260  
31 6     6   30 use constant SF_HOSTS_BYTES => 9;
  6         8  
  6         282  
32 6     6   27 use constant SF_SHUTDOWN => 10;
  6         12  
  6         237  
33 6     6   29 use constant SF_REQ_BY_SOCK => 11;
  6         15  
  6         16190  
34              
35             # Spawn a new PoCo::Client::DNS session. This basically is a
36             # constructor, but it isn't named "new" because it doesn't create a
37             # usable object. Instead, it spawns the object off as a session.
38              
39             sub spawn {
40 8     8 1 13027 my $type = shift;
41 8 100       351 croak "$type requires an even number of parameters" if @_ % 2;
42 7         33 my %params = @_;
43              
44 7         21 my $alias = delete $params{Alias};
45 7 100       28 $alias = "resolver" unless $alias;
46              
47 7         18 my $timeout = delete $params{Timeout};
48 7 100       41 $timeout = 90 unless $timeout;
49              
50 7         16 my $nameservers = delete $params{Nameservers};
51 7         123 my $resolver = Net::DNS::Resolver->new();
52 7   50     2187 $nameservers ||= [ $resolver->nameservers() ];
53              
54 7         263 my $hosts = delete $params{HostsFile};
55              
56 7 100       150 croak(
57             "$type doesn't know these parameters: ", join(', ', sort keys %params)
58             ) if scalar keys %params;
59              
60             # TODO - SF_NAMESERVERS isn't used right now. It exists for future
61             # expansion.
62              
63 6         33 my $self = bless [
64             $alias, # SF_ALIAS
65             $timeout, # SF_TIMEOUT
66             $nameservers, # SF_NAMESERVERS
67             $resolver, # SF_RESOLVER
68             $hosts, # SF_HOSTS_FILE
69             0, # SF_HOSTS_MTIME
70             0, # SF_HOSTS_CTIME
71             0, # SF_HOSTS_INODE
72             { }, # SF_HOSTS_CACHE
73             0, # SF_HOSTS_BYTES
74             0, # SF_SHUTDOWN
75             ], $type;
76              
77             # Set the list of nameservers, if one was supplied.
78             # May redundantly reset itself.
79 6         45 $self->[SF_RESOLVER]->nameservers(@$nameservers);
80              
81 6         315 POE::Session->create(
82             object_states => [
83             $self => {
84             _default => "_dns_default",
85             _start => "_dns_start",
86             _stop => "_dns_stop",
87             got_dns_response => "_dns_response",
88             resolve => "_dns_resolve",
89             send_request => "_dns_do_request",
90             shutdown => "_dns_shutdown",
91             },
92             ],
93             );
94              
95 6         1194 return $self;
96             }
97              
98             # Public method interface.
99              
100             sub resolve {
101 13     13 1 83806 my $self = shift;
102 13 100       158 croak "resolve() needs an even number of parameters" if @_ % 2;
103 12         64 my %args = @_;
104              
105 12 100       139 croak "resolve() must include an 'event'" unless exists $args{event};
106 11 100       115 croak "resolve() must include a 'context'" unless exists $args{context};
107 10 100       115 croak "resolve() must include a 'host'" unless exists $args{host};
108              
109 9         50 $poe_kernel->call( $self->[SF_ALIAS], "resolve", \%args );
110              
111 9         242 return undef;
112             }
113              
114             sub shutdown {
115 0     0 1 0 my $self = shift;
116 0         0 $poe_kernel->call( $self->[SF_ALIAS], "shutdown" );
117             }
118              
119             # Start the resolver session. Record the parameters which were
120             # validated in spawn(), create the internal resolver object, and set
121             # an alias which we'll be known by.
122              
123             sub _dns_start {
124 6     6   2295 my ($object, $kernel) = @_[OBJECT, KERNEL];
125 6         33 $kernel->alias_set($object->[SF_ALIAS]);
126             }
127              
128             # Dummy handler to avoid ASSERT_DEFAULT problems.
129              
130       5     sub _dns_stop {
131             # do nothing
132             }
133              
134             # Receive a request. Version 4 API. This uses extra reference counts
135             # to keep the client sessions alive until responses are ready.
136              
137             sub _dns_resolve {
138 68     68   12486 my ($self, $kernel, $sender, $event, $host, $type, $class) =
139             @_[OBJECT, KERNEL, SENDER, ARG0, ARG1, ARG2, ARG3];
140              
141 68         207 my $debug_info =
142             "in Client::DNS request at $_[CALLER_FILE] line $_[CALLER_LINE]\n";
143              
144 68         91 my ($api_version, $context, $timeout);
145              
146 0         0 my @nameservers;
147              
148             # Version 3 API. Pass the entire request as a hash.
149 68 100       213 if (ref($event) eq 'HASH') {
    100          
150 16         71 my %args = %$event;
151              
152 16         40 $type = delete $args{type};
153 16 100       48 $type = "A" unless $type;
154              
155 16         30 $class = delete $args{class};
156 16 100       53 $class = "IN" unless $class;
157              
158 16         37 $event = delete $args{event};
159 16 100       55 die "Must include an 'event' $debug_info" unless $event;
160              
161 15         28 $context = delete $args{context};
162 15 100       48 die "Must include a 'context' $debug_info" unless $context;
163              
164 14         24 $timeout = delete $args{timeout};
165              
166 14 100       45 @nameservers = @{delete $args{nameservers}} if $args{nameservers};
  1         4  
167              
168 14         25 $host = delete $args{host};
169 14 100       51 die "Must include a 'host' $debug_info" unless $host;
170              
171 13         28 $api_version = 3;
172             }
173              
174             # Parse user args from the magical $response format. Version 2 API.
175              
176             elsif (ref($event) eq "ARRAY") {
177 4         4 $context = $event;
178 4         8 $event = shift @$context;
179 4         7 $api_version = 2;
180             }
181              
182             # Whee. Version 1 API.
183              
184             else {
185 48         63 $context = [ ];
186 48         80 $api_version = 1;
187             }
188              
189 65 100       160 @nameservers = @{ $self->[SF_NAMESERVERS] } unless @nameservers;
  64         190  
190              
191             # Default the request's timeout.
192 65 100       210 $timeout = $self->[SF_TIMEOUT] unless $timeout;
193              
194             # Set an extra reference on the sender so it doesn't go away.
195 65         182 $kernel->refcount_increment($sender->ID, __PACKAGE__);
196              
197             # If it's an IN type A request, check /etc/hosts or the equivalent.
198             # -><- This is not always the right thing to do, but it's more right
199             # more often than never checking at all.
200              
201 65 100 100     2011 if (($type eq "A" or $type eq "AAAA") and $class eq "IN") {
      66        
202 11         42 my $address = $self->_check_hosts_file($host, $type);
203              
204 11 100       35 if (defined $address) {
205             # Pretend the request went through a name server.
206              
207 4         36 my $packet = Net::DNS::Packet->new($address, $type, "IN");
208 4         883 $packet->push(
209             "answer",
210             Net::DNS::RR->new(
211             Name => $host,
212             TTL => 1,
213             Class => $class,
214             Type => $type,
215             Address => $address,
216             )
217             );
218              
219             # Send the response immediately, and return.
220              
221 4         7576 _send_response(
222             api_ver => $api_version,
223             sender => $sender,
224             event => $event,
225             host => $host,
226             type => $type,
227             class => $class,
228             context => $context,
229             response => $packet,
230             error => "",
231             );
232              
233 4         266 return;
234             }
235             }
236              
237             # We are here. Yield off to the state where the request will be
238             # sent. This is done so that the do-it state can yield or delay
239             # back to itself for retrying.
240              
241 61         115 my $now = time();
242 61         604 $kernel->call(
243             $self->[SF_ALIAS],
244             send_request => {
245             sender => $sender,
246             event => $event,
247             host => $host,
248             type => $type,
249             class => $class,
250             context => $context,
251             started => $now,
252             ends => $now + $timeout,
253             api_ver => $api_version,
254             nameservers => \@nameservers,
255             }
256             );
257             }
258              
259             # Perform the real request. May recurse to perform retries.
260              
261             sub _dns_do_request {
262 74     74   4609 my ($self, $kernel, $req) = @_[OBJECT, KERNEL, ARG0];
263              
264             # Did the request time out?
265 74         144 my $remaining = $req->{ends} - time();
266 74 50       186 if ($remaining <= 0) {
267 0         0 _send_response(
268             %$req,
269             response => undef,
270             error => "timeout",
271             );
272 0         0 return;
273             }
274              
275             # Send the request.
276              
277             $self->[SF_RESOLVER]->nameservers(
278 74 50       118 @{ $req->{nameservers} || $self->[SF_NAMESERVERS] }
  74         389  
279             );
280             my $resolver_ref = $self->[SF_RESOLVER]->bgsend(
281             $req->{host},
282             $req->{type},
283             $req->{class}
284 74         3323 );
285              
286             # The request failed? Attempt to retry.
287              
288 74 50       86093 unless ($resolver_ref) {
289 0 0       0 $remaining = 1 if $remaining > 1;
290 0         0 $kernel->delay_add(send_request => $remaining, $req);
291 0         0 return;
292             }
293              
294 74         116 my $resolver_socket;
295              
296 74 50       202 if ( ref( $resolver_ref ) eq 'IO::Select' ) {
297 74         249 ($resolver_socket) = [ $resolver_ref->handles ]->[0][0];
298             }
299             else {
300 0         0 $resolver_socket = $resolver_ref;
301             }
302              
303 74         3416 $req->{RES_REF} = $resolver_ref;
304              
305             # Set a timeout for the request, and watch the response socket for
306             # activity.
307              
308 74         253 $self->[SF_REQ_BY_SOCK]->{$resolver_socket} = $req;
309              
310 74         340 $kernel->delay($resolver_socket, $remaining / 2, $resolver_socket);
311 74         22727 $kernel->select_read($resolver_socket, 'got_dns_response');
312              
313             # Save the socket for pre-emptive shutdown.
314 74         6193 $req->{resolver_socket} = $resolver_socket;
315             }
316              
317             # A resolver query timed out. Keep trying until we run out of time.
318             # Also, if the top nameserver is the one we tried, then cycle the
319             # nameservers.
320              
321             sub _dns_default {
322 13     13   7655 my ($self, $kernel, $event, $args) = @_[OBJECT, KERNEL, ARG0, ARG1];
323 13         27 my $socket = $args->[0];
324              
325 13 50 33     94 return unless defined($socket) and $event eq $socket;
326              
327 13         36 my $req = delete $self->[SF_REQ_BY_SOCK]->{$socket};
328 13 50       36 return unless $req;
329              
330             # Stop watching the socket.
331 13         41 $kernel->select_read($socket);
332              
333             # No more time remaining? We must time out.
334 13         1309 my $remaining = $req->{ends} - time();
335 13 50       38 if ($remaining <= 0) {
336 0         0 _send_response(
337             %$req,
338             response => undef,
339             error => "timeout",
340             );
341 0         0 return;
342             }
343              
344             # There remains time. Let's try again.
345              
346             # The nameserver we tried has failed us. If it's the top
347             # nameserver in Net::DNS's list, then send it to the back and retry.
348             # TODO - What happens if they all fail forever?
349              
350 13         62 my @nameservers = @{ $req->{nameservers} };
  13         43  
351 13         25 push @nameservers, shift(@nameservers);
352 13         60 $self->[SF_RESOLVER]->nameservers(@nameservers);
353 13         572 $req->{nameservers} = \@nameservers;
354              
355             # Retry.
356 13         51 $kernel->yield(send_request => $req);
357              
358             # Don't accidentally handle signals.
359             # Only meaningful for old POEs.
360 13         978 return;
361             }
362              
363             # A resolver query generated a response. Post the reply back.
364              
365             sub _dns_response {
366 61     61   1002040 my ($self, $kernel, $socket) = @_[OBJECT, KERNEL, ARG0];
367              
368 61         273 my $req = delete $self->[SF_REQ_BY_SOCK]->{$socket};
369 61 50       221 return unless $req;
370              
371             # Turn off the timeout for this request, and stop watching the
372             # resolver connection.
373 61         230 $kernel->delay($socket);
374 61         18154 $kernel->select_read($socket);
375              
376 61         11749 my $handle = delete $req->{RES_REF};
377              
378             # Read the DNS response.
379 61         381 my $packet = $self->[SF_RESOLVER]->bgread($handle);
380              
381             # Set the packet's answerfrom field, if the packet was received ok
382             # and an answerfrom isn't already included. This uses the
383             # documented peerhost() method
384              
385 61 50 33     475020 if (defined $packet and !defined $packet->answerfrom) {
386 0         0 my $answerfrom = getpeername($socket);
387 0 0       0 if (defined $answerfrom) {
388 0         0 $answerfrom = (unpack_sockaddr_in($answerfrom))[1];
389 0         0 $answerfrom = inet_ntoa($answerfrom);
390 0         0 $packet->answerfrom($answerfrom);
391             }
392             }
393              
394             # Send the response.
395             _send_response(
396 61         954 %$req,
397             response => $packet,
398             error => $self->[SF_RESOLVER]->errorstring(),
399             );
400             }
401              
402             sub _dns_shutdown {
403 0     0   0 my ($self, $kernel) = @_[OBJECT, KERNEL];
404              
405             # Clean up all pending socket timeouts and selects.
406 0         0 foreach my $socket (keys %{$self->[SF_REQ_BY_SOCK]}) {
  0         0  
407 0         0 DEBUG and warn "SHT: Shutting down resolver socket $socket";
408 0         0 my $req = delete $self->[SF_REQ_BY_SOCK]->{$socket};
409              
410 0         0 $kernel->delay($socket);
411 0         0 $kernel->select($req->{resolver_socket});
412              
413             # Let the client session go.
414 0         0 DEBUG and warn "SHT: Releasing sender ", $req->{sender}->ID;
415 0         0 $poe_kernel->refcount_decrement($req->{sender}->ID, __PACKAGE__);
416             }
417              
418             # Clean out our global timeout.
419 0         0 $kernel->delay(send_request => undef);
420              
421             # Clean up our global alias.
422 0         0 DEBUG and warn "SHT: Resolver removing alias $self->[SF_ALIAS]";
423 0         0 $kernel->alias_remove($self->[SF_ALIAS]);
424              
425 0         0 $self->[SF_SHUTDOWN] = 1;
426             }
427              
428             # Send a response. Fake a postback for older API versions. Send a
429             # nice, tidy hash for new ones. Also decrement the reference count
430             # that's keeping the requester session alive.
431              
432             sub _send_response {
433 65     65   826 my %args = @_;
434              
435             # Simulate a postback for older API versions.
436              
437 65         149 my $api_version = delete $args{api_ver};
438 65 100       188 if ($api_version < 3) {
439             $poe_kernel->post(
440             $args{sender}, $args{event},
441 52         348 [ $args{host}, $args{type}, $args{class}, @{$args{context}} ],
442 52         178 [ $args{response}, $args{error} ],
443             );
444             }
445              
446             # New, fancy, shiny hash-based response.
447              
448             else {
449             $poe_kernel->post(
450             $args{sender}, $args{event},
451             {
452             host => $args{host},
453             type => $args{type},
454             class => $args{class},
455             context => $args{context},
456             response => $args{response},
457             error => $args{error},
458             }
459 13         155 );
460             }
461              
462             # Let the client session go.
463 65         8353 $poe_kernel->refcount_decrement($args{sender}->ID, __PACKAGE__);
464             }
465              
466             ### NOT A POE EVENT HANDLER
467              
468             sub _check_hosts_file {
469 11     11   23 my ($self, $host, $type) = @_;
470              
471             # Use the hosts file that was specified, or find one.
472 11         15 my $use_hosts_file;
473 11 100       39 if (defined $self->[SF_HOSTS_FILE]) {
474 4         10 $use_hosts_file = $self->[SF_HOSTS_FILE];
475             }
476             else {
477             # Discard the hosts file name if it has disappeared.
478 7 50 66     128 $global_hosts_file = undef if (
479             $global_hosts_file and !-f $global_hosts_file
480             );
481              
482             # Try to find a hosts file if one doesn't exist.
483 7 100       19 unless ($global_hosts_file) {
484 2         4 my @candidates = (
485             "/etc/hosts",
486             );
487              
488 2 50       12 if ( $^O eq 'android' ) {
489 0         0 push @candidates, "/system/etc/hosts";
490             }
491            
492 2 50 33     14 if ($^O eq "MSWin32" or $^O eq "Cygwin") {
493 0         0 my $sys_dir;
494 0   0     0 $sys_dir = $ENV{SystemRoot} || "c:\\Windows";
495 0         0 push(
496             @candidates,
497             "$sys_dir\\System32\\Drivers\\Etc\\hosts",
498             "$sys_dir\\System\\Drivers\\Etc\\hosts",
499             "$sys_dir\\hosts",
500             );
501             }
502              
503 2         7 foreach my $candidate (@candidates) {
504 2 50       40 next unless -f $candidate;
505 2         4 $global_hosts_file = $candidate;
506 2         7 $global_hosts_file =~ s/\\+/\//g;
507 2         4 $self->[SF_HOSTS_MTIME] = 0;
508 2         3 $self->[SF_HOSTS_CTIME] = 0;
509 2         9 $self->[SF_HOSTS_INODE] = 0;
510 2         5 last;
511             }
512             }
513              
514             # We use the global hosts file.
515 7         13 $use_hosts_file = $global_hosts_file;
516             }
517              
518             # Still no hosts file? Don't bother reading it, then.
519 11 50       34 return unless $use_hosts_file;
520              
521             # Blow away our cache if the file doesn't exist.
522 11 100       150 $self->[SF_HOSTS_CACHE] = { } unless -f $use_hosts_file;
523              
524             # Reload the hosts file if times have changed.
525 11         130 my ($inode, $bytes, $mtime, $ctime) = (stat $use_hosts_file)[1, 7, 9,10];
526 11 100 100     161 unless (
      50        
      66        
      50        
      66        
      50        
      66        
527             $self->[SF_HOSTS_MTIME] == ($mtime || -1) and
528             $self->[SF_HOSTS_CTIME] == ($ctime || -1) and
529             $self->[SF_HOSTS_INODE] == ($inode || -1) and
530             $self->[SF_HOSTS_BYTES] == ($bytes || -1)
531             ) {
532 5 100       127 return unless open(HOST, "<", $use_hosts_file);
533              
534 4         13 my %cached_hosts;
535 4         70 while () {
536 17 50       55 next if /^\s*\#/; # skip all-comment lines
537 17 50       61 next if /^\s*$/; # skip empty lines
538 17         26 chomp;
539              
540             # Bare split discards leading and trailing whitespace.
541 17         57 my ($address, @aliases) = split;
542 17 50       50 next unless defined $address;
543              
544 17 100       49 my $type = ($address =~ /:/) ? "AAAA" : "A";
545 17         33 foreach my $alias (@aliases) {
546 21         176 $cached_hosts{$alias}{$type}{$address} = 1;
547             }
548             }
549 4         31 close HOST;
550              
551             # Normalize our cached hosts.
552 4         29 while (my ($alias, $type_rec) = each %cached_hosts) {
553 19         64 while (my ($type, $address_rec) = each %$type_rec) {
554 21         133 $cached_hosts{$alias}{$type} = (keys %$address_rec)[0];
555             }
556             }
557              
558 4         12 $self->[SF_HOSTS_CACHE] = \%cached_hosts;
559 4         14 $self->[SF_HOSTS_MTIME] = $mtime;
560 4         21 $self->[SF_HOSTS_CTIME] = $ctime;
561 4         10 $self->[SF_HOSTS_INODE] = $inode;
562 4         12 $self->[SF_HOSTS_BYTES] = $bytes;
563             }
564              
565             # Return whatever match we have.
566             return unless (
567             (exists $self->[SF_HOSTS_CACHE]{$host}) and
568 10 100 66     63 (exists $self->[SF_HOSTS_CACHE]{$host}{$type})
569             );
570 4         15 return $self->[SF_HOSTS_CACHE]{$host}{$type};
571             }
572              
573             ### NOT A POE EVENT HANDLER
574              
575             sub get_resolver {
576 0     0 1   my $self = shift;
577 0           return $self->[SF_RESOLVER];
578             }
579              
580             1;
581              
582             __END__