use
constant
DEBUG
=>
$ENV
{MOJO_RESOLVER_DEBUG} || 0;
use
constant
IPV6
=>
defined
&Socket::AF_INET6
&&
defined
&Socket::inet_pton
;
has
ioloop
=>
sub
{
Mojo::IOLoop->singleton;
};
has
timeout
=> 3;
my
$DEC_OCTET_RE
=
qr/(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])/
;
my
$IPV4_RE
=
qr/^$DEC_OCTET_RE\.$DEC_OCTET_RE\.$DEC_OCTET_RE\.$DEC_OCTET_RE$/
;
my
$H16_RE
=
qr/[0-9A-Fa-f]{1,4}/
;
my
$LS32_RE
=
qr/(?:$H16_RE:$H16_RE|$IPV4_RE)/
;
my
$IPV6_RE
=
qr/(?:
(?: $H16_RE : ){6} $LS32_RE
| :: (?: $H16_RE : ){5} $LS32_RE
| (?: $H16_RE )? :: (?: $H16_RE : ){4} $LS32_RE
| (?: (?: $H16_RE : ){0,1} $H16_RE )? :: (?: $H16_RE : ){3} $LS32_RE
| (?: (?: $H16_RE : ){0,2} $H16_RE )? :: (?: $H16_RE : ){2} $LS32_RE
| (?: (?: $H16_RE : ){0,3} $H16_RE )? :: $H16_RE : $LS32_RE
| (?: (?: $H16_RE : ){0,4} $H16_RE )? :: $LS32_RE
| (?: (?: $H16_RE : ){0,5} $H16_RE )? :: $H16_RE
| (?: (?: $H16_RE : ){0,6} $H16_RE )? ::
)/
x;
my
$SERVERS
= [
'8.8.8.8'
,
'8.8.4.4'
];
if
(-r
'/etc/resolv.conf'
) {
my
$file
= IO::File->new(
'< /etc/resolv.conf'
);
my
@servers
;
for
my
$line
(<
$file
>) {
if
(
$line
=~ /^nameserver\s+(\S+)$/) {
push
@servers
, $1;
warn
qq/DETECTED DNS SERVER ($1)\n/
if
DEBUG;
}
}
unshift
@$SERVERS
,
@servers
;
}
unshift
@$SERVERS
,
$ENV
{MOJO_DNS_SERVER}
if
$ENV
{MOJO_DNS_SERVER};
my
$CURRENT_SERVER
= 0;
my
$DNS_TYPES
= {
'*'
=> 0x00ff,
A
=> 0x0001,
AAAA
=> 0x001c,
CNAME
=> 0x0005,
MX
=> 0x000f,
NS
=> 0x0002,
PTR
=> 0x000c,
TXT
=> 0x0010
};
our
$LOCALHOST
=
'127.0.0.1'
;
sub
DESTROY {
shift
->_cleanup }
sub
is_ipv4 {
return
1
if
$_
[1] =~
$IPV4_RE
;
return
;
}
sub
is_ipv6 {
return
1
if
$_
[1] =~
$IPV6_RE
;
return
;
}
sub
lookup {
my
(
$self
,
$name
,
$cb
) =
@_
;
weaken
$self
;
return
$self
->ioloop->defer(
sub
{
$self
->
$cb
(
$LOCALHOST
) })
if
$name
eq
'localhost'
;
$self
->resolve(
$name
,
'A'
,
sub
{
my
(
$self
,
$records
) =
@_
;
my
$result
= first {
$_
->[0] eq
'A'
}
@$records
;
return
$self
->
$cb
(
$result
->[1])
if
$result
;
$self
->resolve(
$name
,
'AAAA'
,
sub
{
my
(
$self
,
$records
) =
@_
;
my
$result
= first {
$_
->[0] eq
'AAAA'
}
@$records
;
return
$self
->
$cb
(
$result
->[1])
if
$result
;
$self
->
$cb
();
}
);
}
);
}
sub
resolve {
my
(
$self
,
$name
,
$type
,
$cb
) =
@_
;
my
$t
=
$DNS_TYPES
->{
$type
};
my
$v4
=
$self
->is_ipv4(
$name
);
my
$v6
= IPV6 ?
$self
->is_ipv6(
$name
) : 0;
my
$server
=
$self
->servers;
my
$loop
=
$self
->ioloop;
weaken
$self
;
return
$loop
->defer(
sub
{
$self
->
$cb
([]) })
if
!
$server
|| !
$t
|| (
$t
ne
$DNS_TYPES
->{PTR} && (
$v4
||
$v6
));
warn
"RESOLVE $type $name ($server)\n"
if
DEBUG;
my
$tx
;
do
{
$tx
=
int
rand
0x10000 }
while
(
$self
->{requests}->{
$tx
});
my
$req
=
pack
'nnnnnn'
,
$tx
, 0x0100, 1, 0, 0, 0;
my
@parts
=
split
/\./,
$name
;
if
(
$t
eq
$DNS_TYPES
->{PTR}) {
if
(
$v4
) {
@parts
=
reverse
'arpa'
,
'in-addr'
,
@parts
}
elsif
(
$v6
) {
@parts
=
reverse
'arpa'
,
'ip6'
,
split
//,
unpack
'H32'
,
Socket::inet_pton(Socket::AF_INET6(),
$name
);
}
}
for
my
$part
(
@parts
) {
$req
.=
pack
'C/a*'
,
$part
if
defined
$part
}
$req
.=
pack
'Cnn'
, 0,
$t
, 0x0001;
$self
->_bind(
$server
);
$self
->{requests}->{
$tx
} = {
cb
=>
$cb
,
timer
=>
$loop
->timer(
$self
->
timeout
=>
sub
{
my
$loop
=
shift
;
warn
"RESOLVE TIMEOUT ($server)\n"
if
DEBUG;
$CURRENT_SERVER
++;
$self
->_cleanup;
}
)
};
$loop
->
write
(
$self
->{id} =>
$req
);
}
sub
servers {
my
$self
=
shift
;
if
(
@_
) {
@$SERVERS
=
@_
;
$CURRENT_SERVER
= 0;
}
return
@$SERVERS
if
wantarray
;
$CURRENT_SERVER
= 0
unless
$SERVERS
->[
$CURRENT_SERVER
];
return
$SERVERS
->[
$CURRENT_SERVER
];
}
sub
_bind {
my
(
$self
,
$server
) =
@_
;
return
if
$self
->{id};
my
$loop
=
$self
->ioloop;
weaken
$self
;
$self
->{id} =
$loop
->
connect
(
address
=>
$server
,
port
=> 53,
on_close
=>
sub
{
$self
->_cleanup },
on_error
=>
sub
{
my
$loop
=
shift
;
warn
"RESOLVE FAILURE ($server)\n"
if
DEBUG;
$CURRENT_SERVER
++;
$self
->_cleanup;
},
on_read
=>
sub
{
my
(
$loop
,
$id
,
$chunk
) =
@_
;
my
@packet
=
unpack
'nnnnnna*'
,
$chunk
;
warn
"ANSWERS $packet[3] ($server)\n"
if
DEBUG;
return
unless
my
$r
=
delete
$self
->{requests}->{
$packet
[0]};
my
$content
=
$packet
[6];
for
(1 ..
$packet
[2]) {
my
$n
;
do
{ (
$n
,
$content
) =
unpack
'C/aa*'
,
$content
}
while
(
$n
ne
''
);
$content
= (
unpack
'nna*'
,
$content
)[2];
}
my
@answers
;
for
(1 ..
$packet
[3]) {
(
my
(
$t
,
$ttl
,
$a
),
$content
) =
(
unpack
'nnnNn/aa*'
,
$content
)[1, 3, 4, 5];
my
@answer
= _parse_answer(
$t
,
$a
,
$chunk
,
$content
);
next
unless
@answer
;
push
@answers
, [
@answer
,
$ttl
];
warn
"ANSWER $answer[0] $answer[1]\n"
if
DEBUG;
}
$loop
->drop(
$r
->{timer});
$r
->{cb}->(
$self
, \
@answers
);
},
args
=> {
Proto
=>
'udp'
,
Type
=> SOCK_DGRAM}
);
}
sub
_cleanup {
my
$self
=
shift
;
return
unless
my
$loop
=
$self
->ioloop;
$loop
->drop(
delete
$self
->{id})
if
$self
->{id};
for
my
$tx
(
keys
%{
$self
->{requests}}) {
my
$r
=
delete
$self
->{requests}->{
$tx
};
$r
->{cb}->(
$self
, []);
}
}
sub
_parse_answer {
my
(
$t
,
$a
,
$packet
,
$rest
) =
@_
;
if
(
$t
eq
$DNS_TYPES
->{A}) {
return
A
=>
join
(
'.'
,
unpack
'C4'
,
$a
) }
elsif
(
$t
eq
$DNS_TYPES
->{AAAA}) {
return
AAAA
=>
sprintf
(
'%x:%x:%x:%x:%x:%x:%x:%x'
,
unpack
(
'n*'
,
$a
));
}
elsif
(
$t
eq
$DNS_TYPES
->{TXT}) {
return
TXT
=>
unpack
(
'(C/a*)*'
,
$a
) }
my
$offset
=
length
(
$packet
) -
length
(
$rest
) -
length
(
$a
);
my
$type
;
if
(
$t
eq
$DNS_TYPES
->{CNAME}) {
$type
=
'CNAME'
}
elsif
(
$t
eq
$DNS_TYPES
->{MX}) {
$type
=
'MX'
;
$offset
+= 2;
}
elsif
(
$t
eq
$DNS_TYPES
->{NS}) {
$type
=
'NS'
}
elsif
(
$t
eq
$DNS_TYPES
->{PTR}) {
$type
=
'PTR'
}
return
$type
=> _parse_name(
$packet
,
$offset
)
if
$type
;
return
;
}
sub
_parse_name {
my
(
$packet
,
$offset
) =
@_
;
my
@elements
;
for
(1 .. 128) {
my
$len
=
ord
substr
$packet
,
$offset
++, 1;
if
(
$len
>= 0xc0) {
$offset
= (
unpack
'n'
,
substr
$packet
, ++
$offset
- 2, 2) & 0x3fff;
}
elsif
(
$len
) {
push
@elements
,
substr
$packet
,
$offset
,
$len
;
$offset
+=
$len
;
}
else
{
return
join
'.'
,
@elements
}
}
return
;
}
1;