File Coverage

blib/lib/POE/Component/Client/DNS.pm
Criterion Covered Total %
statement 202 235 85.9
branch 71 90 78.8
condition 24 42 57.1
subroutine 28 31 90.3
pod 4 4 100.0
total 329 402 81.8


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