Skip to content

Instantly share code, notes, and snippets.

@gmr
Last active April 28, 2016 22:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gmr/25e3377c0d3a9fa4592b0f4786d06dd9 to your computer and use it in GitHub Desktop.
Save gmr/25e3377c0d3a9fa4592b0f4786d06dd9 to your computer and use it in GitHub Desktop.
"""
tdns
====
An asynchronous Tornado pycares DNS client wrapper, exporting the full API.
"""
from tornado import concurrent
from tornado import ioloop
import pycares
__version__ = '0.1.0'
__all__ = ['Channel',
'QueryError'
'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']
# 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
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):
"""Destroy the channel when deleting the object instance."""
self._channel.destroy()
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
"""
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
"""
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
"""
future = concurrent.Future()
self._channel.gethostbyaddr(
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
"""
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.QueryError` with the ``ARES_ECANCELLED`` errorno.
"""
self._channel.cancel()
def destroy(self):
"""Destroy the channel. All pending requests will raise a
:exc:`~tdns.QueryError` with the ``ARES_EDESTRUCTION `` errorno.
"""
self._channel.destroy()
def timeout(self, max_timeout):
"""Determines 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
"""
return self._channel.timeout(max_timeout)
def set_local_ip4(self, local_ip):
"""Set the local IPv4 address from which the queries will be sent.
:param str local_ip: IP address
"""
return self._channel.set_local_ipv4(local_ip)
def set_local_ip6(self, local_ip):
"""Set the local IPv6 address from which the queries will be sent.
:param str local_ip: IP address
"""
return self._channel.set_local_ipv6(local_ip)
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
"""
return self._channel.set_local_dev(local_dev)
@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 pycares.
: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(QueryError(errno,
pycares.errno.strerror(errno)))
else:
future.set_result(result)
class QueryError(Exception):
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment