tdns.py
"""
tdns
====
An asynchronous Tornado pycares DNS client wrapper, exporting the full API.
"""
from tornado import concurrent
from tornado import ioloop
import pycares.errno
__version__ = '0.2.0'
__all__ = ['Channel',
'AresError'
'reverse_address',
'QUERY_TYPE_A',
'QUERY_TYPE_AAAA',
'QUERY_TYPE_CNAME',
'QUERY_TYPE_MX',
'QUERY_TYPE_NAPTR',
'QUERY_TYPE_NS',
'QUERY_TYPE_PTR',
'QUERY_TYPE_SOA',
'QUERY_TYPE_SRV',
'QUERY_TYPE_TXT',
'ARES_FLAG_USEVC',
'ARES_FLAG_PRIMARY',
'ARES_FLAG_IGNTC',
'ARES_FLAG_NORECURSE',
'ARES_FLAG_STAYOPEN',
'ARES_FLAG_NOSEARCH',
'ARES_FLAG_NOALIASES',
'ARES_FLAG_NOCHECKRESP',
'ARES_NI_NOFQDN',
'ARES_NI_NUMERICHOST',
'ARES_NI_NAMEREQD',
'ARES_NI_NUMERICSERV',
'ARES_NI_DGRAM',
'ARES_NI_TCP',
'ARES_NI_UDP',
'ARES_NI_SCTP',
'ARES_NI_DCCP',
'ARES_NI_NUMERICSCOPE',
'ARES_NI_LOOKUPHOST',
'ARES_NI_LOOKUPSERVICE',
'ARES_NI_IDN',
'ARES_NI_IDN_ALLOW_UNASSIGNED',
'ARES_NI_IDN_USE_STD3_ASCII_RULES'
'ARES_SUCCESS',
'ARES_ENODATA',
'ARES_EFORMERR',
'ARES_ESERVFAIL',
'ARES_ENOTFOUND',
'ARES_ENOTIMP',
'ARES_EREFUSED',
'ARES_EBADQUERY',
'ARES_EBADNAME',
'ARES_EBADFAMILY',
'ARES_EBADRESP',
'ARES_ECONNREFUSED',
'ARES_ETIMEOUT',
'ARES_EOF',
'ARES_EFILE',
'ARES_ENOMEM',
'ARES_EDESTRUCTION',
'ARES_EBADSTR',
'ARES_EBADFLAGS',
'ARES_ENONAME',
'ARES_EBADHINTS',
'ARES_ENOTINITIALIZED',
'ARES_ELOADIPHLPAPI',
'ARES_EADDRGETNETWORKPARAMS',
'ARES_ECANCELLED']
AresError = pycares.AresError
# Export pycares constants
QUERY_TYPE_A = pycares.QUERY_TYPE_A
QUERY_TYPE_AAAA = pycares.QUERY_TYPE_AAAA
QUERY_TYPE_CNAME = pycares.QUERY_TYPE_CNAME
QUERY_TYPE_MX = pycares.QUERY_TYPE_MX
QUERY_TYPE_NAPTR = pycares.QUERY_TYPE_NAPTR
QUERY_TYPE_NS = pycares.QUERY_TYPE_NS
QUERY_TYPE_PTR = pycares.QUERY_TYPE_PTR
QUERY_TYPE_SOA = pycares.QUERY_TYPE_SOA
QUERY_TYPE_SRV = pycares.QUERY_TYPE_SRV
QUERY_TYPE_TXT = pycares.QUERY_TYPE_TXT
ARES_FLAG_USEVC = pycares.ARES_FLAG_USEVC
ARES_FLAG_PRIMARY = pycares.ARES_FLAG_PRIMARY
ARES_FLAG_IGNTC = pycares.ARES_FLAG_IGNTC
ARES_FLAG_NORECURSE = pycares.ARES_FLAG_NORECURSE
ARES_FLAG_STAYOPEN = pycares.ARES_FLAG_STAYOPEN
ARES_FLAG_NOSEARCH = pycares.ARES_FLAG_NOSEARCH
ARES_FLAG_NOALIASES = pycares.ARES_FLAG_NOALIASES
ARES_FLAG_NOCHECKRESP = pycares.ARES_FLAG_NOCHECKRESP
ARES_NI_NOFQDN = pycares.ARES_NI_NOFQDN
ARES_NI_NUMERICHOST = pycares.ARES_NI_NUMERICHOST
ARES_NI_NAMEREQD = pycares.ARES_NI_NAMEREQD
ARES_NI_NUMERICSERV = pycares.ARES_NI_NUMERICSERV
ARES_NI_DGRAM = pycares.ARES_NI_DGRAM
ARES_NI_TCP = pycares.ARES_NI_TCP
ARES_NI_UDP = pycares.ARES_NI_UDP
ARES_NI_SCTP = pycares.ARES_NI_SCTP
ARES_NI_DCCP = pycares.ARES_NI_DCCP
ARES_NI_NUMERICSCOPE = pycares.ARES_NI_NUMERICSCOPE
ARES_NI_LOOKUPHOST = pycares.ARES_NI_LOOKUPHOST
ARES_NI_LOOKUPSERVICE = pycares.ARES_NI_LOOKUPSERVICE
ARES_NI_IDN = pycares.ARES_NI_IDN
ARES_NI_IDN_ALLOW_UNASSIGNED = pycares.ARES_NI_IDN_ALLOW_UNASSIGNED
ARES_NI_IDN_USE_STD3_ASCII_RULES = pycares.ARES_NI_IDN_USE_STD3_ASCII_RULES
# export pycares.errno constants too
ARES_SUCCESS = pycares.errno.ARES_SUCCESS
ARES_ENODATA = pycares.errno.ARES_ENODATA
ARES_EFORMERR = pycares.errno.ARES_EFORMERR
ARES_ESERVFAIL = pycares.errno.ARES_ESERVFAIL
ARES_ENOTFOUND = pycares.errno.ARES_ENOTFOUND
ARES_ENOTIMP = pycares.errno.ARES_ENOTIMP
ARES_EREFUSED = pycares.errno.ARES_EREFUSED
ARES_EBADQUERY = pycares.errno.ARES_EBADQUERY
ARES_EBADNAME = pycares.errno.ARES_EBADNAME
ARES_EBADFAMILY = pycares.errno.ARES_EBADFAMILY
ARES_EBADRESP = pycares.errno.ARES_EBADRESP
ARES_ECONNREFUSED = pycares.errno.ARES_ECONNREFUSED
ARES_ETIMEOUT = pycares.errno.ARES_ETIMEOUT
ARES_EOF = pycares.errno.ARES_EOF
ARES_EFILE = pycares.errno.ARES_EFILE
ARES_ENOMEM = pycares.errno.ARES_ENOMEM
ARES_EDESTRUCTION = pycares.errno.ARES_EDESTRUCTION
ARES_EBADSTR = pycares.errno.ARES_EBADSTR
ARES_EBADFLAGS = pycares.errno.ARES_EBADFLAGS
ARES_ENONAME = pycares.errno.ARES_ENONAME
ARES_EBADHINTS = pycares.errno.ARES_EBADHINTS
ARES_ENOTINITIALIZED = pycares.errno.ARES_ENOTINITIALIZED
ARES_ELOADIPHLPAPI = pycares.errno.ARES_ELOADIPHLPAPI
ARES_EADDRGETNETWORKPARAMS = pycares.errno.ARES_EADDRGETNETWORKPARAMS
ARES_ECANCELLED = pycares.errno.ARES_ECANCELLED
def reverse_address(ip_address):
"""Returns the reversed representation of an IP address, usually used when
doing PTR queries.
:param str ip_address: IP address to be reversed
:rtype: str
"""
return pycares.reverse_address(ip_address)
class Channel(object):
"""An asynchronous wrapper class for c-ares channels."""
def __init__(self, io_loop=None, **kwargs):
"""Create a new :class:`~tdns.Channel` instance.
:param int flags: Flags controlling the behavior of the resolver. See
constants for available values
:param float timeout: The number of seconds each name server is given
to respond to a query on the first try. The default is five seconds
:param int tries: The number of tries the resolver will try contacting
each name server before giving up. The default is four tries
:param int ndots: The number of dots which must be present in a domain
name for it to be queried for "as is" prior to querying for it with
the default domain extensions appended. The default value is 1
unless set otherwise by ``resolv.conf`` or the ``RES_OPTIONS``
environment variable
:param int tcp_port: The (TCP) port to use for queries.
The default is ``53``
:param int udp_port: The (UDP) port to use for queries.
The default is ``53``
:param list servers: List of nameservers to be used to do the
lookups
:param list domains: The domains to search, instead of the
domains specified in ``resolv.conf`` or the domain derived from the
kernel hostname variable
:param str lookup: The lookups to perform for host queries.
lookups should be set to a string of the characters ``b`` or ``f``,
where ``b`` indicates a DNS lookup and ``f`` indicates a lookup in
the hosts file
:param bool rotate: If set to ``True``, the nameservers are rotated
when doing queries
:param tornado.ioloop.IOLoop io_loop: The IOLoop to use.
The default is `tornado.ioloop.IOLoop.current`
"""
self.io_loop = io_loop or ioloop.IOLoop.current()
self._fds = {}
kwargs['sock_state_cb'] = self._sock_state_cb
self._channel = pycares.Channel(**kwargs)
def __del__(self): # pragma: no cover
"""Destroy the channel when deleting the object instance."""
try:
self._channel.destroy()
except pycares.AresError as e:
# raised when the channel has never been used
pass
def gethostbyname(self, name, family):
"""Retrieves host information corresponding to a host name from a host
database.
:param str name: Name to query
:param int family: Socket family
:raises: :exc:`~tdns.AresError`
"""
future = concurrent.Future()
self._channel.gethostbyname(
name, family,
lambda result, errno: self._process_result(result, errno, future))
return future
def gethostbyaddr(self, addr):
"""Retrieves the host information corresponding to a network address.
:param str addr: Network address to query
:rtype: str
:raises: :exc:`~tdns.AresError`
"""
future = concurrent.Future()
self._channel.gethostbyaddr(
addr,
lambda result, errno: self._process_result(result, errno, future))
return future
def getnameinfo(self, name, port, flags):
"""Provides protocol-independent name resolution from an address to a
host name and from a port number to the service name.
:param str name: Name to query
:param int port: Port of the service to query
:param int flags: Query flags, see the NI flags section
:raises: :exc:`~tdns.AresError`
"""
future = concurrent.Future()
self._channel.getnameinfo(
(name, port), flags,
lambda result, errno: self._process_result(result, errno, future))
return future
def query(self, name, query_type):
"""Do a DNS query of the specified type. Available types:
- :data:`tdns.QUERY_TYPE_A`
- :data:`tdns.QUERY_TYPE_AAAA`
- :data:`tdns.QUERY_TYPE_CNAME`
- :data:`tdns.QUERY_TYPE_MX`
- :data:`tdns.QUERY_TYPE_NAPTR`
- :data:`tdns.QUERY_TYPE_NS`
- :data:`tdns.QUERY_TYPE_PTR`
- :data:`tdns.QUERY_TYPE_SOA`
- :data:`tdns.QUERY_TYPE_SRV`
- :data:`tdns.QUERY_TYPE_TXT`
:param str name: Name to query
:param int query_type: Type of query to perform.
Return Types:
- A and AAAA: ``ares_query_simple_result``, fields:
- host
- ttl
- CNAME: ``ares_query_cname_result``, fields:
- cname
- ttl
- MX: ``ares_query_mx_result``, fields:
- host
- priority
- ttl
- NAPTR: ``ares_query_naptr_result``, fields:
- order
- preference
- flags
- service
- regex
- replacement
- ttl
- NS: ``ares_query_ns_result``, fields:
- host
- ttl
- PTR: ``ares_query_ptr_result``, fields:
- name
- ttl
- SOA: ``ares_query_soa_result``, fields:
- nsmane
- hostmaster
- serial
- refresh
- retry
- expires
- minttl
- ttl
- SRV: ``ares_query_srv_result``, fields:
- host
- port
- priority
- weight
- ttl
- TXT: ``ares_query_txt_result``, fields:
- text
- ttl
:raises: :exc:`~tdns.AresError`
"""
future = concurrent.Future()
self._channel.query(
name, query_type,
lambda result, errno: self._process_result(result, errno, future))
return future
def cancel(self):
"""Cancel any pending query on this channel. All pending requests will
raise a :exc:`~tdns.AresError` with the ``ARES_ECANCELLED`` errorno.
:raises: :exc:`~tdns.AresError`
"""
self._channel.cancel()
def destroy(self):
"""Destroy the channel. All pending requests will raise a
:exc:`~tdns.AresError` with the ``ARES_EDESTRUCTION`` errorno.
:raises: :exc:`~tdns.AresError`
"""
self._channel.destroy()
def timeout(self, max_timeout):
"""Set the maximum time for which the caller should wait before
invoking process_fd to process timeouts. If the ``max_timeout``
parameter is specified, it is stored on the channel and the appropriate
value is then returned.
:param float max_timeout: Maximum timeout
:rtype: float
:raises: :exc:`~tdns.AresError`
:raises: ValueError
"""
return self._channel.timeout(max_timeout)
def set_local_dev(self, local_dev):
"""Set the local ethernet device from which the queries will be sent.
:param str local_dev: Network device name
:raises: :exc:`~tdns.AresError`
:raises: ValueError
"""
return self._channel.set_local_dev(local_dev)
def set_local_ip(self, local_ip):
"""Set the local IPv4 or IPv6 address from which the queries will be
sent.
:param str local_ip: IP address
:raises: :exc:`~tdns.AresError`
:raises: ValueError
"""
return self._channel.set_local_ip(local_ip)
@property
def servers(self):
"""List of nameservers to use for DNS queries
:rtype: list
"""
return self._channel.servers
@servers.setter
def servers(self, servers):
"""Set the list of nameservers to use for DNS queries
:param list servers: The servers to use
"""
self._channel.servers = servers
def _sock_state_cb(self, fd, readable, writable):
state = ((ioloop.IOLoop.READ if readable else 0) |
(ioloop.IOLoop.WRITE if writable else 0))
if not state:
self.io_loop.remove_handler(fd)
del self._fds[fd]
elif fd in self._fds:
self.io_loop.update_handler(fd, state)
self._fds[fd] = state
else:
self.io_loop.add_handler(fd, self._handle_events, state)
self._fds[fd] = state
def _handle_events(self, fd, events):
read_fd = pycares.ARES_SOCKET_BAD
write_fd = pycares.ARES_SOCKET_BAD
if events & ioloop.IOLoop.READ:
read_fd = fd
if events & ioloop.IOLoop.WRITE:
write_fd = fd
self._channel.process_fd(read_fd, write_fd)
@staticmethod
def _process_result(result, errno, future):
"""Common method for processing pycares responses.
:param mixed result: The result from the pycares call
:param int errno: The error number if any
:param tornado.concurrent.Future future: The future to assign the
result to
"""
if errno is not None:
future.set_exception(AresError(errno,
pycares.errno.strerror(errno)))
else:
future.set_result(result)