File Coverage

blib/lib/ParaDNS.pm
Criterion Covered Total %
statement 75 162 46.3
branch 15 58 25.8
condition 12 29 41.3
subroutine 12 19 63.1
pod 1 6 16.6
total 115 274 41.9


line stmt bran cond sub pod time code
1             package ParaDNS;
2              
3             # This is the query class - it is really just an encapsulation of the
4             # hosts you want to query, plus the callback. All the hard work is done
5             # in ParaDNS::Resolver.
6              
7             our $VERSION = '2.0';
8             our $TIMEOUT = $ENV{PARADNS_TIMEOUT} || 10;
9             our $REQUERY = $ENV{PARADNS_REQUERY} || 2;
10              
11 1         6 use fields qw(
12             client
13             hosts
14             num_hosts
15             callback
16             finished
17             results
18             start
19             type
20             nameservers
21 1     1   1856 );
  1         1715  
22 1     1   106 use strict;
  1         3  
  1         52  
23              
24 1     1   17 no warnings 'deprecated';
  1         2  
  1         43  
25              
26 1     1   729 use ParaDNS::Resolver;
  1         4  
  1         47  
27             # note currently disabled as not everything works right
28 1     1   5 use constant XS_AVAILABLE => eval { require ParaDNS::XS; };
  1         2  
  1         2  
  1         483  
29 1   50 1   5 use constant NO_DNS => ($ENV{NODNS} || 0);
  1         2  
  1         62  
30              
31             my %RESOLVER;
32              
33 1   50 1   6 use constant TRACE_LEVEL => ($ENV{PARADNS_DEBUG} || 0);
  1         1  
  1         93  
34 1   50 1   5 use constant INTERNAL_CACHE => !($ENV{PARADNS_NO_CACHE} || 0);
  1         2  
  1         57  
35              
36 1     1   6 use constant CACHE_CLEAN_INTERVAL => 60;
  1         2  
  1         2164  
37              
38             my %cache;
39             my $cache_cleanup;
40              
41             sub trace {
42 0     0 0 0 return unless TRACE_LEVEL;
43 0         0 my $level = shift;
44 0         0 print STDERR ("$ENV{PARADNS_DEBUG}/$level [$$] dns lookup: @_");
45             }
46              
47             sub get_resolver {
48 4     4 0 5 if (INTERNAL_CACHE) {
49 4   66     20 $cache_cleanup ||= Danga::Socket->AddTimer(CACHE_CLEAN_INTERVAL, \&_cache_cleanup);
50             }
51 4 50       40 if (XS_AVAILABLE) {
52 0 0       0 return 1 if $RESOLVER{$$};
53 0         0 ParaDNS::XS::setup();
54 0         0 $RESOLVER{$$} = 1;
55             }
56             else {
57 4         7 my $servers = shift;
58 4   66     31 $RESOLVER{$$} ||= ParaDNS::Resolver->new($servers);
59             }
60             }
61              
62             sub _cache_cleanup {
63 0     0   0 my $now = time;
64            
65 0         0 foreach my $type (keys(%cache)) {
66 0         0 my @to_delete;
67            
68 0         0 keys %{$cache{$type}}; # reset internal iterator
  0         0  
69 0         0 for my $query (keys(%{$cache{$type}})) {
  0         0  
70 0         0 REC:
71 0         0 for my $rec (@{ $cache{$type}{$query} }) {
72 0         0 my $t = $rec->{timeout};
73 0 0       0 if ($t < $now) {
74 0         0 push @to_delete, $query;
75 0         0 last REC;
76             }
77             }
78             }
79            
80 0         0 foreach my $q (@to_delete) {
81 0         0 delete $cache{$type}{$q};
82             }
83             }
84              
85 0         0 $cache_cleanup = Danga::Socket->AddTimer(CACHE_CLEAN_INTERVAL, \&_cache_cleanup);
86             }
87              
88             sub new {
89 4     4 1 1875 my ParaDNS $self = shift;
90 4         18 my %options = ( type => "A", @_ );
91              
92 4         15 my $now = time;
93            
94 4         7 my $client = $options{client};
95 4 50       12 $client->pause_read() if $client;
96            
97 4 50       23 $self = fields::new($self) unless ref $self;
98              
99 4 50       4344 $self->{hosts} = $options{hosts} ? $options{hosts} : [ $options{host} ];
100 4 50       16 $self->{nameservers} = $options{nameservers} ? $options{nameservers} : '';
101 4   50     6 $self->{num_hosts} = scalar(@{$self->{hosts}}) || "No hosts supplied";
102 4         8 $self->{client} = $client;
103 4   50     13 $self->{callback} = $options{callback} || die "No callback given";
104 4         8 $self->{finished} = $options{finished};
105 4         6 $self->{results} = {};
106 4         7 $self->{start} = $now;
107 4         12 my $type = $self->{type} = $options{type};
108            
109 4 50       11 trace(2, "Nameservers set to: @{$self->{nameservers}}\n")
  0         0  
110             if $self->{nameservers};
111              
112 4         5 if (NO_DNS) {
113             $self->run_callback("NXDNS", $_) for @{ $self->{hosts} };
114             return $self;
115             }
116            
117 4         12 my $resolver = get_resolver($self->{nameservers});
118              
119             # check for cache hits
120 4         7 for my $host (@{ $self->{hosts} }) {
  4         10  
121 4 50 66     23 if (INTERNAL_CACHE &&
  1   33     8  
122             exists($cache{$type}{$host}) &&
123             @{$cache{$type}{$host}} > 0 &&
124             $cache{$type}{$host}[0]{timeout} >= $now)
125             {
126             Danga::Socket->AddTimer(0, sub {
127 0     0   0 $self->run_cache_callback($type, $host);
128 0         0 });
129             }
130             else {
131             # not cached - do lookup
132 4 50       13 if (XS_AVAILABLE) {
133             my $callback = sub {
134 0     0   0 $self->run_xs_callback(@_);
135 0         0 };
136 0         0 my $id;
137 0 0 0     0 if ($type eq "A" && $host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
138 0         0 $id = ParaDNS::XS::dnsquery("PTR", "$4.$3.$2.$1.in-addr.arpa", $callback);
139             }
140             else {
141 0         0 $id = ParaDNS::XS::dnsquery($type, $host, $callback);
142             }
143 0 0       0 if (!defined($id)) {
144             # lookup failed for some bizarre reason - should never happen
145 0 0       0 $client->continue_read() if $client;
146 0         0 return;
147             }
148             }
149             else {
150 4         11 $cache{$type}{$host} = [];
151 4 50       17 if (!$resolver->query_type($self, $type, $host)) {
152             # lookup failed for some bizarre reason - should never happen
153 4 50       12 $client->continue_read() if $client;
154 4         25 return;
155             }
156             }
157             }
158             }
159              
160 0         0 return $self;
161             }
162              
163             sub run_cache_callback {
164 0     0 0 0 my ParaDNS $self = shift;
165 0         0 my ($type, $host) = @_;
166 0         0 for my $rec (@{ $cache{$type}{$host} }) {
  0         0  
167 0         0 my $result = $rec->{value};
168 0         0 my $ttl = $rec->{ttl};
169 0         0 $self->{results}{$host} = $result;
170 0   0     0 $ttl ||= 0;
171 0         0 trace(2, "got cached $type $host => $result\n") if TRACE_LEVEL >= 2;
172 0         0 $self->{callback}->($result, $host, $ttl);
173             }
174             }
175              
176             sub run_callback {
177 0     0 0 0 my ParaDNS $self = shift;
178 0         0 my ($result, $query, $ttl) = @_;
179 0         0 $self->{results}{$query} = $result;
180 0 0       0 if (INTERNAL_CACHE && defined($ttl)) {
181             # store in cache
182 0         0 push @{$cache{$self->{type}}{$query}},
  0         0  
183             {
184             timeout => time + $ttl,
185             ttl => $ttl,
186             value => $result,
187             };
188             }
189 0   0     0 $ttl ||= 0;
190 0         0 trace(2, "got $query => $result\n") if TRACE_LEVEL >= 2;
191 0         0 $self->{callback}->($result, $query, $ttl);
192             }
193              
194             my %type_to_host = (
195             PTR => 'dname',
196             A => 'address',
197             AAAA => 'address',
198             TXT => 'txtdata',
199             NS => 'dname',
200             CNAME => 'dname',
201             );
202              
203             sub run_xs_callback {
204 0     0 0 0 my ParaDNS $self = shift;
205 0         0 my $data = shift;
206             #warn("$$ run_xs_callback status: $data->{status} => $data->{error}\n");
207 0 0       0 if ($data->{status} > 1) {
208 0 0       0 if ($data->{questions}) {
209             #warn("$$ run_xs_callback $data->{error} with questions\n");
210 0         0 for my $q (@{$data->{questions}}) {
  0         0  
211 0         0 trace(2, "got $q->{question} => $data->{error}\n") if TRACE_LEVEL >= 2;
212 0         0 $self->{results}{$q->{question}} = $data->{error};
213 0         0 $self->{callback}->($data->{error}, $q->{question});
214             }
215             }
216             else {
217             #warn("$$ run_xs_callback $data->{error} with no questions\n");
218 0         0 for my $host (@{$self->{hosts}}) {
  0         0  
219 0 0       0 next if exists $self->{results}{$host};
220 0         0 trace(2, "got $host => $data->{error}\n") if TRACE_LEVEL >= 2;
221 0         0 $self->{results}{$host} = $data->{error};
222 0         0 $self->{callback}->($data->{error}, $host);
223             }
224             }
225 0         0 return;
226             }
227              
228 0         0 my $query = $data->{questions}[0]{question};
229 0 0       0 if ($data->{questions}[0]{type} eq 'PTR') {
230 0         0 $query =~ s/^(\d+)\.(\d+)\.(\d+)\.(\d+)\.in-addr\.arpa/$4.$3.$2.$1/;
231             }
232 0         0 for my $answer (@{$data->{answers}}) {
  0         0  
233 0         0 my $result;
234 0 0       0 if (my $param = $type_to_host{$answer->{type}}) {
    0          
235 0         0 $result = $answer->{$param};
236             }
237             elsif ($answer->{type} eq "MX") {
238 0         0 $result = [$answer->{exchange}, $answer->{preference}];
239             }
240             else {
241 0         0 die "Unimplemented query type: $answer->{type}";
242             }
243 0         0 $self->run_callback($result, $query, $answer->{ttl});
244             }
245 0 0       0 if (!$self->{results}{$query}) {
246 0         0 $self->{results}{$query} = 'NXDOMAIN';
247 0         0 trace(2, "got $query => NXDOMAIN\n") if TRACE_LEVEL >= 2;
248 0         0 $self->{callback}->("NXDOMAIN", $query);
249             }
250             }
251              
252             sub DESTROY {
253 4     4   5 my ParaDNS $self = shift;
254 4         6 my $now = time;
255 4         6 my $num_hosts = @{$self->{hosts}};
  4         9  
256 4 50       5 if ($num_hosts > keys(%{$self->{results}})) {
  4         15  
257             # not enough results came back
258 4         5 foreach my $host (@{$self->{hosts}}) {
  4         10  
259 4 50       12 next if exists($self->{results}{$host});
260 4 50       14 if ($host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
261 0 0       0 next if exists($self->{results}{"$4.$3.$2.$1.in-addr.arpa"});
262             }
263 4         21 print STDERR "DNS failure looking for $host after " . ($now - $self->{start}) . " secs (looked for $num_hosts, got " . keys(%{$self->{results}}) . ")\n";
  4         350  
264 4         28 $self->{callback}->("NXDOMAIN", $host);
265             }
266             }
267 4 50       2396 $self->{client}->continue_read() if $self->{client};
268 4 50       32 if ($self->{finished}) {
269 0           $self->{finished}->();
270             }
271             }
272              
273             1;
274              
275             =head1 NAME
276              
277             ParaDNS - a DNS lookup class for the Danga::Socket framework
278              
279             =head1 SYNOPSIS
280              
281             ParaDNS->new(
282             callback => sub { print "Got result $_[0] for query $_[1]\n" },
283             host => 'google.com',
284             );
285              
286             =head1 DESCRIPTION
287              
288             This module performs asynchronous DNS lookups, making use of a single UDP
289             socket (unlike Net::DNS's bgsend/bgread combination). It uses the Danga::Socket
290             framework for high performance.
291              
292             Currently this module will only perform A or PTR lookups. A rDNS (PTR) lookup
293             will be performed if the host matches the regexp: C.
294              
295             The lookups time out after 15 seconds.
296              
297             =head1 API
298              
299             =head2 C<< ParaDNS->new( %options ) >>
300              
301             Create a new DNS query. You do not need to store the resulting object as this
302             class is all done with callbacks.
303              
304             Example:
305              
306             ParaDNS->new(
307             callback => sub { print "Got result: $_[0]\n" },
308             host => 'google.com',
309             );
310              
311             =over 4
312              
313             =item B<[required]> C
314              
315             The callback to call when results come in. This should be a reference to a
316             subroutine. The callback receives three parameters - the result of the DNS lookup,
317             the host that was looked up, and the TTL (in seconds).
318              
319             =item C
320              
321             A host name to lookup. Note that if the hostname is a dotted quad of numbers then
322             a reverse DNS (PTR) lookup is performend.
323              
324             =item C
325              
326             An array-ref list of hosts to lookup.
327              
328             B One of either C or C is B.
329              
330             =item C
331              
332             It is possible to specify a client object which you wish to "pause" for reading
333             until your DNS result returns. The client will be issued the C<< ->pause_read >>
334             method when the query is issued, and the C<< ->continue_read >> method when the
335             query returns.
336              
337             This is used in Qpsmtpd where we want to wait until the DNS query returns before
338             accepting more data from the client.
339              
340             =item C
341              
342             You can specify one of: I<"A">, I<"AAAA">, I<"PTR">, I<"CNAME">, I<"NS"> or
343             I<"TXT"> here. Other types may be supported in the future. See C<%type_to_host>
344             in C for details, though more complex queries (e.g. SRV) may
345             require a slightly more complex solution.
346              
347             A PTR query is automatically issued if the host looks like an IP address.
348              
349             =item C
350              
351             Normally, this module uses the name servers that are default for your system.
352             You can specify an array-ref list of name servers to query.
353              
354             =back
355              
356             =head1 Environment Variables
357              
358             =head2 PARADNS_TIMEOUT
359              
360             Default: 10
361              
362             Number of seconds to wait for a query to come back.
363              
364             =head2 PARADNS_REQUERY
365              
366             Default: 2
367              
368             Number of times to re-send a query when it times out.
369              
370             =head2 PARADNS_NO_CACHE
371              
372             Provides the ability to turn off the in-memory cache. Set to 1 to disable.
373              
374             =head2 PARADNS_DEBUG
375              
376             Provides internal debugging sent to STDERR. Set to 1 or higher to see more
377             debugging output.
378              
379             =head1 Stand-alone Use
380              
381             Normal usage of ParaDNS is within another application that already uses the
382             Danga::Socket framework. However if you wish to use this as a script to just
383             issue thousands of DNS queries then you need to do a little more work.
384             First, you need to set the SetPostLoopCallback, then issue the appropriate
385             ParaDNS->new() call with your queries, and then launch the Danga event
386             loop.
387              
388             Eg:
389              
390             Danga::Socket->SetPostLoopCallback(
391             sub {
392             my $dmap = shift;
393             for my $fd (keys %$dmap) {
394             my $pob = $dmap->{$fd};
395             if ($pob->isa('ParaDNS::Resolver')) {
396             return 1 if $pob->pending;
397             }
398             }
399             return 0; # causes EventLoop to exit
400             });
401              
402             # Call ParaDNS->new() with your parameters
403              
404             Danga::Socket->EventLoop();
405              
406             =head1 LICENSE
407              
408             This module is licensed under the same terms as perl itself.
409              
410             =head1 AUTHOR
411              
412             Matt Sergeant, .
413              
414             =cut