Last active
April 28, 2016 22:19
-
-
Save gmr/25e3377c0d3a9fa4592b0f4786d06dd9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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