File Coverage

blib/lib/IO/Async/Resolver/DNS.pm
Criterion Covered Total %
statement 79 84 94.0
branch 17 28 60.7
condition 7 13 53.8
subroutine 15 16 93.7
pod 0 2 0.0
total 118 143 82.5


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2011-2012 -- leonerd@leonerd.org.uk
5              
6             package IO::Async::Resolver::DNS;
7              
8 5     5   419267 use strict;
  5         13  
  5         210  
9 5     5   29 use warnings;
  5         11  
  5         240  
10              
11             our $VERSION = '0.04';
12              
13 5     5   4803 use IO::Async::Resolver;
  5         444912  
  5         196  
14              
15 5     5   62 use Carp;
  5         10  
  5         406  
16 5     5   5271 use Net::DNS;
  5         336123  
  5         648  
17              
18 5     5   4668 use List::UtilsBy qw( weighted_shuffle_by );
  5         8900  
  5         6936  
19              
20             =head1 NAME
21              
22             C - resolve DNS queries using C
23              
24             =head1 SYNOPSIS
25              
26             use IO::Async::Loop;
27             use IO::Async::Resolver::DNS;
28            
29             my $loop = IO::Async::Loop->new;
30             my $resolver = $loop->resolver;
31            
32             $resolver->res_query(
33             dname => "cpan.org",
34             type => "MX",
35             on_resolved => sub {
36             my ( $pkt ) = @_;
37            
38             foreach my $mx ( $pkt->answer ) {
39             next unless $mx->type eq "MX";
40            
41             printf "preference=%d exchange=%s\n",
42             $mx->preference, $mx->exchange;
43             }
44             $loop->stop;
45             },
46             on_error => sub { die "Cannot resolve - $_[-1]\n" },
47             );
48            
49             $loop->run;
50              
51             =head1 DESCRIPTION
52              
53             This module extends the L class with extra methods and
54             resolver functions to perform DNS-specific resolver lookups. It does not
55             directly provide any methods or functions of its own.
56              
57             These functions are provided for performing DNS-specific lookups, to obtain
58             C or C records, for example. For regular name resolution, the usual
59             C and C methods on the standard
60             C should be used.
61              
62             If L is installed then it will be used for actually sending
63             and receiving DNS packets, in preference to a internally-constructed
64             L object. C will be more efficient and
65             shares its implementation with the standard resolver used by the rest of the
66             system. C reimplements the logic itself, so it may have
67             differences in behaviour from that provided by F. The ability to
68             use the latter is provided to allow for an XS-free dependency chain, or for
69             other situations where C is not available.
70              
71             =head2 Record Extraction
72              
73             If certain record type queries are made, extra information is returned to the
74             C continuation, containing the results from the DNS packet in a
75             more useful form. This information will be in a list of extra values following
76             the packet value
77              
78             $on_resolved->( $pkt, @data )
79              
80             The type of the elements in C<@data> will depend on the DNS record query type:
81              
82             =over 4
83              
84             =cut
85              
86             sub _extract
87             {
88 6     6   21 my ( $pkt, $type ) = @_;
89              
90 6 50       196 my $code = __PACKAGE__->can( "_extract_$type" ) or return ( $pkt );
91              
92 6         41 return $code->( $pkt );
93             }
94              
95             =item * A and AAAA
96              
97             The C or C records will be unpacked and returned in a list of
98             strings.
99              
100             @data = ( "10.0.0.1",
101             "10.0.0.2" );
102              
103             @data = ( "fd00:0:0:0:0:0:0:1" );
104              
105             =cut
106              
107             *_extract_A = \&_extract_addresses;
108             *_extract_AAAA = \&_extract_addresses;
109             sub _extract_addresses
110             {
111 4     4   7 my ( $pkt ) = @_;
112              
113 4         7 my @addrs;
114              
115 4         55 foreach my $rr ( $pkt->answer ) {
116 8 100 66     219 push @addrs, $rr->address if $rr->type eq "A" or $rr->type eq "AAAA";
117             }
118              
119 4         112 return ( $pkt, @addrs );
120             }
121              
122             =item * PTR
123              
124             The C records will be unpacked and returned in a list of domain names.
125              
126             @data = ( "foo.example.com" );
127              
128             =cut
129              
130             sub _extract_PTR
131             {
132 0     0   0 my ( $pkt ) = @_;
133              
134 0         0 my @names;
135              
136 0         0 foreach my $rr ( $pkt->answer ) {
137 0 0       0 push @names, $rr->ptrdname if $rr->type eq "PTR";
138             }
139              
140 0         0 return ( $pkt, @names );
141             }
142              
143             =item * MX
144              
145             The C records will be unpacked, in order of C, and returned in
146             a list of HASH references. Each HASH reference will contain keys called
147             C and C. If the exchange domain name is included in the
148             DNS C data, then the HASH reference will also include a key called
149             C
, its value containing a list of C and C record C
150             fields.
151              
152             @data = ( { exchange => "mail.example.com",
153             preference => 10,
154             address => [ "10.0.0.1", "fd00:0:0:0:0:0:0:1" ] } );
155              
156             =cut
157              
158             sub _extract_MX
159             {
160 1     1   3 my ( $pkt ) = @_;
161              
162 1         2 my @mx;
163             my %additional;
164              
165 1         18 foreach my $rr ( $pkt->additional ) {
166 3 50 66     229 push @{ $additional{$rr->name}{address} }, $rr->address if $rr->type eq "A" or $rr->type eq "AAAA";
  3         87  
167             }
168              
169 1         42 foreach my $ans ( sort { $a->preference <=> $b->preference } grep { $_->type eq "MX" } $pkt->answer ) {
  1         35  
  2         33  
170 2         25 my $exchange = $ans->exchange;
171 2         59 push @mx, { exchange => $exchange, preference => $ans->preference };
172 2 100       24 $mx[-1]{address} = $additional{$exchange}{address} if $additional{$exchange}{address};
173             }
174 1         9 return ( $pkt, @mx );
175             }
176              
177             =item * SRV
178              
179             The C records will be unpacked and sorted first by order of priority,
180             then by a weighted shuffle by weight, and returned in a list of HASH
181             references. Each HASH reference will contain keys called C,
182             C, C and C. If the target domain name is included in the
183             DNS C data, then the HASH reference will also contain a key called
184             C
, its value containing a list of C and C record C
185             fields.
186              
187             @data = ( { priority => 10,
188             weight => 10,
189             target => "server1.service.example.com",
190             port => 1234,
191             address => [ "10.0.1.1" ] } );
192              
193             =cut
194              
195             sub _extract_SRV
196             {
197 1     1   10 my ( $pkt ) = @_;
198              
199 1         12 my @srv;
200             my %additional;
201              
202 1         16 foreach my $rr ( $pkt->additional ) {
203 4 50 33     251 push @{ $additional{$rr->name}{address} }, $rr->address if $rr->type eq "A" or $rr->type eq "AAAA";
  4         61  
204             }
205              
206 1         40 my %srv_by_prio;
207             # Need to work in two phases. Split by priority then shuffle within
208 1         7 foreach my $ans ( grep { $_->type eq "SRV" } $pkt->answer ) {
  5         52  
209 5         39 push @{ $srv_by_prio{ $ans->priority } }, $ans;
  5         25  
210             }
211              
212 1         34 foreach my $prio ( sort { $a <=> $b } keys %srv_by_prio ) {
  3         9  
213 3 100   5   12 foreach my $ans ( weighted_shuffle_by { $_->weight || 1 } @{ $srv_by_prio{$prio} } ) {
  5         42  
  3         24  
214 5         155 my $target = $ans->target;
215 5         184 push @srv, { priority => $ans->priority,
216             weight => $ans->weight,
217             target => $target,
218             port => $ans->port };
219 5 100       99 $srv[-1]{address} = $additional{$target}{address} if $additional{$target}{address};
220             }
221             }
222 1         60 return ( $pkt, @srv );
223             }
224              
225             =back
226              
227             =cut
228              
229             =head1 RESOLVER METHODS
230              
231             =cut
232              
233             =head2 $resolver->res_query( %params )
234              
235             Performs a resolver query on the name, class and type, and invokes a
236             continuation when a result is obtained.
237              
238             Takes the following named parameters:
239              
240             =over 8
241              
242             =item dname => STRING
243              
244             Domain name to look up
245              
246             =item type => STRING
247              
248             Name of the record type to look up (e.g. C)
249              
250             =item class => STRING
251              
252             Name of the record class to look up. Defaults to C so normally this
253             argument is not required.
254              
255             =item on_resolved => CODE
256              
257             Continuation which is invoked after a successful lookup. Will be passed a
258             L object containing the result.
259              
260             $on_resolved->( $pkt )
261              
262             For certain query types, this continuation may also be passed extra data in a
263             list after the C<$pkt>
264              
265             $on_resolved->( $pkt, @data )
266              
267             See the B section above for more detail.
268              
269             =item on_error => CODE
270              
271             Continuation which is invoked after a failed lookup.
272              
273             =back
274              
275             =cut
276              
277             sub IO::Async::Resolver::res_query
278             {
279 4     4 0 24261 my $self = shift;
280 4         28 my %args = @_;
281              
282 4 50       33 my $dname = $args{dname} or croak "Expected 'dname'";
283 4   50     36 my $class = $args{class} || "IN";
284 4 50       17 my $type = $args{type} or croak "Expected 'type'";
285              
286 4         12 my $on_resolved = $args{on_resolved};
287 4 50       15 ref $on_resolved or croak "Expected 'on_resolved' to be a reference";
288              
289             $self->resolve(
290             type => "res_query",
291             data => [ $dname, $class, $type ],
292             on_resolved => sub {
293 4     4   9180769 my ( $data ) = @_;
294 4         131 my $pkt = Net::DNS::Packet->new( \$data );
295 4         17163 $on_resolved->( _extract( $pkt, $type ) );
296             },
297 4         51 on_error => $args{on_error},
298             );
299             }
300              
301             =head2 $resolver->res_search( %params )
302              
303             Performs a resolver query on the name, class and type, and invokes a
304             continuation when a result is obtained. Identical to C except that
305             it additionally implements the default domain name search behaviour.
306              
307             =cut
308              
309             sub IO::Async::Resolver::res_search
310             {
311 2     2 0 5428 my $self = shift;
312 2         15 my %args = @_;
313              
314 2 50       18 my $dname = $args{dname} or croak "Expected 'dname'";
315 2   50     37 my $class = $args{class} || "IN";
316 2 50       10 my $type = $args{type} or croak "Expected 'type'";
317              
318 2         7 my $on_resolved = $args{on_resolved};
319 2 50       8 ref $on_resolved or croak "Expected 'on_resolved' to be a reference";
320              
321             $self->resolve(
322             type => "res_search",
323             data => [ $dname, $class, $type ],
324             on_resolved => sub {
325 2     2   6890 my ( $data ) = @_;
326 2         35 my $pkt = Net::DNS::Packet->new( \$data );
327 2         725 $on_resolved->( _extract( $pkt, $type ) );
328             },
329 2         42 on_error => $args{on_error},
330             );
331             }
332              
333             # We'd prefer to use libresolv to actually talk DNS as it'll be more efficient
334             # and more standard to the OS
335             my @impls = qw(
336             LibResolvImpl
337             NetDNSImpl
338             );
339              
340             while( !defined &res_query ) {
341             die "Unable to load an IO::Async::Resolver::DNS implementation\n" unless @impls;
342             eval { require "IO/Async/Resolver/DNS/" . shift(@impls) . ".pm" };
343             }
344              
345             IO::Async::Resolver::register_resolver res_query => \&res_query;
346             IO::Async::Resolver::register_resolver res_search => \&res_search;
347              
348             =head1 AUTHOR
349              
350             Paul Evans
351              
352             =cut
353              
354             0x55AA;