File Coverage

blib/lib/EV/cares.pm
Criterion Covered Total %
statement 147 147 100.0
branch 79 80 98.7
condition 10 14 71.4
subroutine 22 22 100.0
pod 9 9 100.0
total 267 272 98.1


line stmt bran cond sub pod time code
1             package EV::cares;
2 19     19   2638517 use 5.012;
  19         76  
3 19     19   110 use strict;
  19         34  
  19         641  
4 19     19   143 use warnings;
  19         36  
  19         1168  
5 19     19   1555 use EV;
  19         5972  
  19         1222  
6              
7             BEGIN {
8 19     19   95 our $VERSION = '0.02';
9 19     19   176 use XSLoader;
  19         68  
  19         753  
10 19         22293 XSLoader::load __PACKAGE__, $VERSION;
11             }
12              
13 19     19   169 use Exporter 'import';
  19         48  
  19         51059  
14              
15             our @EXPORT_OK;
16             our %EXPORT_TAGS;
17              
18             $EXPORT_TAGS{status} = [qw(
19             ARES_SUCCESS ARES_ENODATA ARES_EFORMERR ARES_ESERVFAIL ARES_ENOTFOUND
20             ARES_ENOTIMP ARES_EREFUSED ARES_EBADQUERY ARES_EBADNAME ARES_EBADFAMILY
21             ARES_EBADRESP ARES_ECONNREFUSED ARES_ETIMEOUT ARES_EOF ARES_EFILE
22             ARES_ENOMEM ARES_EDESTRUCTION ARES_EBADSTR ARES_EBADFLAGS ARES_ENONAME
23             ARES_EBADHINTS ARES_ENOTINITIALIZED ARES_ECANCELLED ARES_ESERVICE
24             ARES_ENOSERVER
25             )];
26              
27             $EXPORT_TAGS{types} = [qw(
28             T_A T_NS T_CNAME T_SOA T_PTR T_MX T_TXT T_AAAA T_SRV T_NAPTR
29             T_DS T_RRSIG T_DNSKEY T_TLSA T_SVCB T_HTTPS T_CAA T_ANY
30             )];
31              
32             $EXPORT_TAGS{classes} = [qw(C_IN C_CHAOS C_HS C_ANY)];
33              
34             $EXPORT_TAGS{flags} = [qw(
35             ARES_FLAG_USEVC ARES_FLAG_PRIMARY ARES_FLAG_IGNTC ARES_FLAG_NORECURSE
36             ARES_FLAG_STAYOPEN ARES_FLAG_NOSEARCH ARES_FLAG_NOALIASES ARES_FLAG_NOCHECKRESP
37             ARES_FLAG_EDNS ARES_FLAG_NO_DFLT_SVR ARES_FLAG_DNS0x20
38             )];
39              
40             $EXPORT_TAGS{ai} = [qw(
41             ARES_AI_CANONNAME ARES_AI_NUMERICHOST ARES_AI_PASSIVE ARES_AI_NUMERICSERV
42             ARES_AI_V4MAPPED ARES_AI_ALL ARES_AI_ADDRCONFIG ARES_AI_NOSORT
43             )];
44              
45             $EXPORT_TAGS{ni} = [qw(
46             ARES_NI_NOFQDN ARES_NI_NUMERICHOST ARES_NI_NAMEREQD ARES_NI_NUMERICSERV
47             ARES_NI_DGRAM ARES_NI_TCP ARES_NI_UDP
48             )];
49              
50             $EXPORT_TAGS{families} = [qw(AF_INET AF_INET6 AF_UNSPEC)];
51              
52             {
53             my %seen;
54             @EXPORT_OK = grep { !$seen{$_}++ } map { @$_ } values %EXPORT_TAGS;
55             $EXPORT_TAGS{all} = [@EXPORT_OK];
56             }
57              
58             # ptr_name($ip) -> reverse-lookup name (.in-addr.arpa for IPv4, .ip6.arpa for IPv6)
59             # Pure-Perl, no resolver needed.
60             sub ptr_name {
61 11     11 1 286136 my ($ip) = @_;
62 11         80 require Carp;
63 11 100       163 Carp::croak("ptr_name: missing IP") unless defined $ip;
64 10 100       79 if (my @oct = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
65 6         13 for my $o (@oct) {
66 18 100 100     188 Carp::croak("ptr_name: leading-zero octet '$o' is ambiguous (octal vs decimal)")
67             if length($o) > 1 && $o =~ /^0/;
68 17 100       270 Carp::croak("ptr_name: invalid IPv4 octet '$o'") if $o > 255;
69             }
70 4         32 return "$oct[3].$oct[2].$oct[1].$oct[0].in-addr.arpa";
71             }
72 4 100       17 if ($ip =~ /:/) {
73 3         794 require Socket;
74 3 100       8673 my $packed = Socket::inet_pton(Socket::AF_INET6(), $ip)
75             or Carp::croak("ptr_name: invalid IPv6 '$ip'");
76 2         24 my @nibbles = CORE::reverse(split //, unpack 'H*', $packed);
77 2         26 return join('.', @nibbles) . '.ip6.arpa';
78             }
79 1         144 Carp::croak("ptr_name: not a recognized IPv4/IPv6 address: '$ip'");
80             }
81              
82             # parse_header($buf) -> hashref of DNS header fields
83             # Useful for inspecting query() raw responses (AD/CD/AA/TC/RA/RD/QR + counts).
84             sub parse_header {
85 7     7 1 5023261 my ($buf) = @_;
86 7         55 require Carp;
87 7 100       205 Carp::croak("parse_header: buffer too short") if length($buf) < 12;
88 6         33 my ($id, $w, $qd, $an, $ns, $ar) = unpack 'nnnnnn', $buf;
89             return {
90 6         107 id => $id,
91             qr => ($w >> 15) & 1,
92             opcode => ($w >> 11) & 0xf,
93             aa => ($w >> 10) & 1,
94             tc => ($w >> 9) & 1,
95             rd => ($w >> 8) & 1,
96             ra => ($w >> 7) & 1,
97             z => ($w >> 6) & 1,
98             ad => ($w >> 5) & 1,
99             cd => ($w >> 4) & 1,
100             rcode => $w & 0xf,
101             qdcount => $qd,
102             ancount => $an,
103             nscount => $ns,
104             arcount => $ar,
105             };
106             }
107              
108             sub resolve_all {
109 7     7 1 194260 my ($self, $names, $cb) = @_;
110 7         53 require Carp;
111 7 100       406 Carp::croak("resolve_all: first argument must be an arrayref")
112             unless ref $names eq 'ARRAY';
113 5 100       167 Carp::croak("resolve_all: callback must be a CODE reference")
114             unless ref $cb eq 'CODE';
115 4 100       188 Carp::croak("resolve_all: resolver is destroyed") if $self->is_destroyed;
116 3 100       14 return $cb->({}) unless @$names;
117 2         4 my %seen;
118 2         12 my @unique = grep { !$seen{$_}++ } @$names;
  4         17  
119 2         3 my %res;
120 2         4 my $pending = @unique;
121 2         5 for my $name (@unique) {
122             $self->resolve($name, sub {
123 3     3   15 my ($status, @addrs) = @_;
124 3         16 $res{$name} = { status => $status, addrs => \@addrs };
125 3 100       31 $cb->(\%res) if --$pending == 0;
126 3         582 });
127             }
128 2         36 return;
129             }
130              
131             sub resolve_ttl_all {
132 5     5 1 7491 my ($self, $names, $cb) = @_;
133 5         30 require Carp;
134 5 100       143 Carp::croak("resolve_ttl_all: first argument must be an arrayref")
135             unless ref $names eq 'ARRAY';
136 4 100       111 Carp::croak("resolve_ttl_all: callback must be a CODE reference")
137             unless ref $cb eq 'CODE';
138 3 100       149 Carp::croak("resolve_ttl_all: resolver is destroyed") if $self->is_destroyed;
139 2 100       5 return $cb->({}) unless @$names;
140 1         2 my %seen;
141 1         2 my @unique = grep { !$seen{$_}++ } @$names;
  3         8  
142 1         2 my %res;
143 1         2 my $pending = @unique;
144 1         2 for my $name (@unique) {
145             $self->resolve_ttl($name, sub {
146 2     2   7 my ($status, @records) = @_;
147 2         6 $res{$name} = { status => $status, records => \@records };
148 2 100       17 $cb->(\%res) if --$pending == 0;
149 2         195 });
150             }
151 1         11 return;
152             }
153              
154             sub reverse_all {
155 7     7 1 240516 my ($self, $ips, $cb) = @_;
156 7         53 require Carp;
157 7 100       305 Carp::croak("reverse_all: first argument must be an arrayref")
158             unless ref $ips eq 'ARRAY';
159 6 100       197 Carp::croak("reverse_all: callback must be a CODE reference")
160             unless ref $cb eq 'CODE';
161 5 100       172 Carp::croak("reverse_all: resolver is destroyed") if $self->is_destroyed;
162 4 100       14 return $cb->({}) unless @$ips;
163 3         15 my %seen;
164 3         8 my @unique = grep { !$seen{$_}++ } @$ips;
  8         31  
165             # validate every IP upfront. reverse() croaks on invalid input; if we
166             # discovered that mid-loop we would have already dispatched queries 1..k
167             # whose inner callbacks hold $cb and decrement $pending, but $pending
168             # would never reach 0 (we never dispatched k+1..N) -- the completion
169             # callback would be silently orphaned.
170 3         720 require Socket;
171 3         5292 for my $ip (@unique) {
172 6 100 100     388 Carp::croak("reverse_all: invalid IP '$ip'")
173             unless Socket::inet_pton(Socket::AF_INET(), $ip)
174             || Socket::inet_pton(Socket::AF_INET6(), $ip);
175             }
176 1         2 my %res;
177 1         2 my $pending = @unique;
178 1         4 for my $ip (@unique) {
179             $self->reverse($ip, sub {
180 2     2   9 my ($status, @hosts) = @_;
181 2         12 $res{$ip} = { status => $status, hosts => \@hosts };
182 2 100       25 $cb->(\%res) if --$pending == 0;
183 2         190 });
184             }
185 1         18 return;
186             }
187              
188             sub getaddrinfo_all {
189 5     5 1 7742 my ($self, $nodes, $service, $hints, $cb) = @_;
190 5         36 require Carp;
191 5 100       201 Carp::croak("getaddrinfo_all: first argument must be an arrayref")
192             unless ref $nodes eq 'ARRAY';
193 4 100       161 Carp::croak("getaddrinfo_all: callback must be a CODE reference")
194             unless ref $cb eq 'CODE';
195 3 100       200 Carp::croak("getaddrinfo_all: resolver is destroyed") if $self->is_destroyed;
196 2 100       10 return $cb->({}) unless @$nodes;
197 1         2 my %seen;
198 1         3 my @unique = grep { !$seen{$_}++ } @$nodes;
  3         14  
199 1         2 my %res;
200 1         2 my $pending = @unique;
201 1         3 for my $node (@unique) {
202             $self->getaddrinfo($node, $service, $hints, sub {
203 2     2   9 my ($status, @addrs) = @_;
204 2         11 $res{$node} = { status => $status, addrs => \@addrs };
205 2 100       46 $cb->(\%res) if --$pending == 0;
206 2         237 });
207             }
208 1         15 return;
209             }
210              
211             # is_busy: true iff there are pending queries on this resolver. Cheap
212             # wrapper for the most common active_queries comparison.
213 8     8 1 201612 sub is_busy { $_[0]->active_queries > 0 }
214              
215             # wait_idle($timeout_seconds): pump the EV loop until either all of this
216             # resolver's pending queries complete or the timeout elapses. Returns
217             # true if the channel drained, false on timeout. Useful in mostly-
218             # synchronous scripts that want to ensure callbacks have run before
219             # proceeding. Returns immediately if the resolver is already idle.
220             #
221             # Picks up a custom EV::Loop passed to new(loop => $loop): the timer and
222             # the run() call are dispatched on the same loop the resolver's watchers
223             # are armed on. Without this, custom-loop resolvers would hang because
224             # EV::run/EV::timer always target the default loop.
225             sub wait_idle {
226 6     6 1 1426 my ($self, $timeout) = @_;
227 6         46 require Carp;
228 6 100       260 Carp::croak("wait_idle: resolver is destroyed") if $self->is_destroyed;
229 5 100       46 return 1 unless $self->active_queries;
230 1   50     3 $timeout //= 30;
231 1         7 my $expired;
232 1   33     12 my $loop = $self->loop // EV::default_loop;
233 1     1   9 my $timer = $loop->timer($timeout, 0, sub { $expired = 1 });
  1         24  
234 1   66     7 while ($self->active_queries && !$expired) {
235 1         997319 $loop->run(EV::RUN_ONCE);
236             }
237             # Look at the resolver's state rather than $expired: if the timer and
238             # the last query callback both fire in the same RUN_ONCE iteration,
239             # $expired is set but the channel did drain. Don't lie about that.
240 1 50       25 return $self->active_queries ? 0 : 1;
241             }
242              
243             sub search_all {
244 10     10 1 26032604 my ($self, $names, $type, $class_or_cb, $cb) = @_;
245 10         104 require Carp;
246             # search_all($names, $type, $cb) -> 4 args (class default C_IN)
247             # search_all($names, $type, $class, $cb) -> 5 args
248 10         25 my $class;
249 10 100       40 if (@_ == 4) {
    100          
250 5         11 $cb = $class_or_cb;
251             } elsif (@_ == 5) {
252 4         6 $class = $class_or_cb;
253 4         14 require Scalar::Util;
254 4 100       152 Carp::croak("search_all: class must be an integer (C_IN, C_CHAOS, ...)")
255             unless Scalar::Util::looks_like_number($class);
256             } else {
257 1         120 Carp::croak("search_all: usage: \$r->search_all(\\\@names, \$type, [\$class,] \$cb)");
258             }
259 8 100       337 Carp::croak("search_all: first argument must be an arrayref")
260             unless ref $names eq 'ARRAY';
261 7 100       268 Carp::croak("search_all: callback must be a CODE reference")
262             unless ref $cb eq 'CODE';
263 5 100       195 Carp::croak("search_all: resolver is destroyed") if $self->is_destroyed;
264 4 100       16 return $cb->({}) unless @$names;
265 2         4 my %seen;
266 2         7 my @unique = grep { !$seen{$_}++ } @$names;
  4         21  
267 2         4 my %res;
268 2         12 my $pending = @unique;
269 2         7 for my $name (@unique) {
270             my $inner = sub {
271 3     3   35487 my ($status, @records) = @_;
272 3         24 $res{$name} = { status => $status, records => \@records };
273 3 100       66 $cb->(\%res) if --$pending == 0;
274 3         19 };
275 3 100       653 defined $class
276             ? $self->search($name, $type, $class, $inner)
277             : $self->search($name, $type, $inner);
278             }
279 2         35 return;
280             }
281              
282             1;
283              
284             __END__