|
- '''greendns - non-blocking DNS support for Eventlet
- '''
-
- # Portions of this code taken from the gogreen project:
- # http://github.com/slideinc/gogreen
- #
- # Copyright (c) 2005-2010 Slide, Inc.
- # All rights reserved.
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions are
- # met:
- #
- # * Redistributions of source code must retain the above copyright
- # notice, this list of conditions and the following disclaimer.
- # * Redistributions in binary form must reproduce the above
- # copyright notice, this list of conditions and the following
- # disclaimer in the documentation and/or other materials provided
- # with the distribution.
- # * Neither the name of the author nor the names of other
- # contributors may be used to endorse or promote products derived
- # from this software without specific prior written permission.
- #
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- import re
- import struct
- import sys
-
- import eventlet
- from eventlet import patcher
- from eventlet.green import _socket_nodns
- from eventlet.green import os
- from eventlet.green import time
- from eventlet.green import select
- import six
-
-
- def import_patched(module_name):
- # Import cycle note: it's crucial to use _socket_nodns here because
- # regular evenlet.green.socket imports *this* module and if we imported
- # it back we'd end with an import cycle (socket -> greendns -> socket).
- # We break this import cycle by providing a restricted socket module.
- modules = {
- 'select': select,
- 'time': time,
- 'os': os,
- 'socket': _socket_nodns,
- }
- return patcher.import_patched(module_name, **modules)
-
-
- dns = import_patched('dns')
- for pkg in dns.__all__:
- setattr(dns, pkg, import_patched('dns.' + pkg))
- dns.rdtypes.__all__.extend(['dnskeybase', 'dsbase', 'txtbase'])
- for pkg in dns.rdtypes.__all__:
- setattr(dns.rdtypes, pkg, import_patched('dns.rdtypes.' + pkg))
- for pkg in dns.rdtypes.IN.__all__:
- setattr(dns.rdtypes.IN, pkg, import_patched('dns.rdtypes.IN.' + pkg))
- for pkg in dns.rdtypes.ANY.__all__:
- setattr(dns.rdtypes.ANY, pkg, import_patched('dns.rdtypes.ANY.' + pkg))
- del import_patched
-
-
- socket = _socket_nodns
-
- DNS_QUERY_TIMEOUT = 10.0
- HOSTS_TTL = 10.0
-
- EAI_EAGAIN_ERROR = socket.gaierror(socket.EAI_AGAIN, 'Lookup timed out')
- EAI_NONAME_ERROR = socket.gaierror(socket.EAI_NONAME, 'Name or service not known')
- # EAI_NODATA was removed from RFC3493, it's now replaced with EAI_NONAME
- # socket.EAI_NODATA is not defined on FreeBSD, probably on some other platforms too.
- # https://lists.freebsd.org/pipermail/freebsd-ports/2003-October/005757.html
- EAI_NODATA_ERROR = EAI_NONAME_ERROR
- if (os.environ.get('EVENTLET_DEPRECATED_EAI_NODATA', '').lower() in ('1', 'y', 'yes')
- and hasattr(socket, 'EAI_NODATA')):
- EAI_NODATA_ERROR = socket.gaierror(socket.EAI_NODATA, 'No address associated with hostname')
-
-
- def is_ipv4_addr(host):
- """Return True if host is a valid IPv4 address"""
- if not isinstance(host, six.string_types):
- return False
- try:
- dns.ipv4.inet_aton(host)
- except dns.exception.SyntaxError:
- return False
- else:
- return True
-
-
- def is_ipv6_addr(host):
- """Return True if host is a valid IPv6 address"""
- if not isinstance(host, six.string_types):
- return False
- host = host.split('%', 1)[0]
- try:
- dns.ipv6.inet_aton(host)
- except dns.exception.SyntaxError:
- return False
- else:
- return True
-
-
- def is_ip_addr(host):
- """Return True if host is a valid IPv4 or IPv6 address"""
- return is_ipv4_addr(host) or is_ipv6_addr(host)
-
-
- class HostsAnswer(dns.resolver.Answer):
- """Answer class for HostsResolver object"""
-
- def __init__(self, qname, rdtype, rdclass, rrset, raise_on_no_answer=True):
- """Create a new answer
-
- :qname: A dns.name.Name instance of the query name
- :rdtype: The rdatatype of the query
- :rdclass: The rdataclass of the query
- :rrset: The dns.rrset.RRset with the response, must have ttl attribute
- :raise_on_no_answer: Whether to raise dns.resolver.NoAnswer if no
- answer.
- """
- self.response = None
- self.qname = qname
- self.rdtype = rdtype
- self.rdclass = rdclass
- self.canonical_name = qname
- if not rrset and raise_on_no_answer:
- raise dns.resolver.NoAnswer()
- self.rrset = rrset
- self.expiration = (time.time() +
- rrset.ttl if hasattr(rrset, 'ttl') else 0)
-
-
- class HostsResolver(object):
- """Class to parse the hosts file
-
- Attributes
- ----------
-
- :fname: The filename of the hosts file in use.
- :interval: The time between checking for hosts file modification
- """
-
- LINES_RE = re.compile(r"""
- \s* # Leading space
- ([^\r\n#]*?) # The actual match, non-greedy so as not to include trailing space
- \s* # Trailing space
- (?:[#][^\r\n]+)? # Comments
- (?:$|[\r\n]+) # EOF or newline
- """, re.VERBOSE)
-
- def __init__(self, fname=None, interval=HOSTS_TTL):
- self._v4 = {} # name -> ipv4
- self._v6 = {} # name -> ipv6
- self._aliases = {} # name -> canonical_name
- self.interval = interval
- self.fname = fname
- if fname is None:
- if os.name == 'posix':
- self.fname = '/etc/hosts'
- elif os.name == 'nt':
- self.fname = os.path.expandvars(
- r'%SystemRoot%\system32\drivers\etc\hosts')
- self._last_load = 0
- if self.fname:
- self._load()
-
- def _readlines(self):
- """Read the contents of the hosts file
-
- Return list of lines, comment lines and empty lines are
- excluded.
-
- Note that this performs disk I/O so can be blocking.
- """
- try:
- with open(self.fname, 'rb') as fp:
- fdata = fp.read()
- except (IOError, OSError):
- return []
-
- udata = fdata.decode(errors='ignore')
-
- return six.moves.filter(None, self.LINES_RE.findall(udata))
-
- def _load(self):
- """Load hosts file
-
- This will unconditionally (re)load the data from the hosts
- file.
- """
- lines = self._readlines()
- self._v4.clear()
- self._v6.clear()
- self._aliases.clear()
- for line in lines:
- parts = line.split()
- if len(parts) < 2:
- continue
- ip = parts.pop(0)
- if is_ipv4_addr(ip):
- ipmap = self._v4
- elif is_ipv6_addr(ip):
- if ip.startswith('fe80'):
- # Do not use link-local addresses, OSX stores these here
- continue
- ipmap = self._v6
- else:
- continue
- cname = parts.pop(0).lower()
- ipmap[cname] = ip
- for alias in parts:
- alias = alias.lower()
- ipmap[alias] = ip
- self._aliases[alias] = cname
- self._last_load = time.time()
-
- def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,
- tcp=False, source=None, raise_on_no_answer=True):
- """Query the hosts file
-
- The known rdtypes are dns.rdatatype.A, dns.rdatatype.AAAA and
- dns.rdatatype.CNAME.
-
- The ``rdclass`` parameter must be dns.rdataclass.IN while the
- ``tcp`` and ``source`` parameters are ignored.
-
- Return a HostAnswer instance or raise a dns.resolver.NoAnswer
- exception.
- """
- now = time.time()
- if self._last_load + self.interval < now:
- self._load()
- rdclass = dns.rdataclass.IN
- if isinstance(qname, six.string_types):
- name = qname
- qname = dns.name.from_text(qname)
- else:
- name = str(qname)
- name = name.lower()
- rrset = dns.rrset.RRset(qname, rdclass, rdtype)
- rrset.ttl = self._last_load + self.interval - now
- if rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.A:
- addr = self._v4.get(name)
- if not addr and qname.is_absolute():
- addr = self._v4.get(name[:-1])
- if addr:
- rrset.add(dns.rdtypes.IN.A.A(rdclass, rdtype, addr))
- elif rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.AAAA:
- addr = self._v6.get(name)
- if not addr and qname.is_absolute():
- addr = self._v6.get(name[:-1])
- if addr:
- rrset.add(dns.rdtypes.IN.AAAA.AAAA(rdclass, rdtype, addr))
- elif rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.CNAME:
- cname = self._aliases.get(name)
- if not cname and qname.is_absolute():
- cname = self._aliases.get(name[:-1])
- if cname:
- rrset.add(dns.rdtypes.ANY.CNAME.CNAME(
- rdclass, rdtype, dns.name.from_text(cname)))
- return HostsAnswer(qname, rdtype, rdclass, rrset, raise_on_no_answer)
-
- def getaliases(self, hostname):
- """Return a list of all the aliases of a given cname"""
- # Due to the way store aliases this is a bit inefficient, this
- # clearly was an afterthought. But this is only used by
- # gethostbyname_ex so it's probably fine.
- aliases = []
- if hostname in self._aliases:
- cannon = self._aliases[hostname]
- else:
- cannon = hostname
- aliases.append(cannon)
- for alias, cname in six.iteritems(self._aliases):
- if cannon == cname:
- aliases.append(alias)
- aliases.remove(hostname)
- return aliases
-
-
- class ResolverProxy(object):
- """Resolver class which can also use /etc/hosts
-
- Initialise with a HostsResolver instance in order for it to also
- use the hosts file.
- """
-
- def __init__(self, hosts_resolver=None, filename='/etc/resolv.conf'):
- """Initialise the resolver proxy
-
- :param hosts_resolver: An instance of HostsResolver to use.
-
- :param filename: The filename containing the resolver
- configuration. The default value is correct for both UNIX
- and Windows, on Windows it will result in the configuration
- being read from the Windows registry.
- """
- self._hosts = hosts_resolver
- self._filename = filename
- self.clear()
-
- def clear(self):
- self._resolver = dns.resolver.Resolver(filename=self._filename)
- self._resolver.cache = dns.resolver.LRUCache()
-
- def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,
- tcp=False, source=None, raise_on_no_answer=True,
- _hosts_rdtypes=(dns.rdatatype.A, dns.rdatatype.AAAA),
- use_network=True):
- """Query the resolver, using /etc/hosts if enabled.
-
- Behavior:
- 1. if hosts is enabled and contains answer, return it now
- 2. query nameservers for qname if use_network is True
- 3. if qname did not contain dots, pretend it was top-level domain,
- query "foobar." and append to previous result
- """
- result = [None, None, 0]
-
- if qname is None:
- qname = '0.0.0.0'
- if isinstance(qname, six.string_types):
- qname = dns.name.from_text(qname, None)
-
- def step(fun, *args, **kwargs):
- try:
- a = fun(*args, **kwargs)
- except Exception as e:
- result[1] = e
- return False
- if a.rrset is not None and len(a.rrset):
- if result[0] is None:
- result[0] = a
- else:
- result[0].rrset.union_update(a.rrset)
- result[2] += len(a.rrset)
- return True
-
- def end():
- if result[0] is not None:
- if raise_on_no_answer and result[2] == 0:
- raise dns.resolver.NoAnswer
- return result[0]
- if result[1] is not None:
- if raise_on_no_answer or not isinstance(result[1], dns.resolver.NoAnswer):
- raise result[1]
- raise dns.resolver.NXDOMAIN(qnames=(qname,))
-
- if (self._hosts and (rdclass == dns.rdataclass.IN) and (rdtype in _hosts_rdtypes)):
- if step(self._hosts.query, qname, rdtype, raise_on_no_answer=False):
- if (result[0] is not None) or (result[1] is not None) or (not use_network):
- return end()
-
- # Main query
- step(self._resolver.query, qname, rdtype, rdclass, tcp, source, raise_on_no_answer=False)
-
- # `resolv.conf` docs say unqualified names must resolve from search (or local) domain.
- # However, common OS `getaddrinfo()` implementations append trailing dot (e.g. `db -> db.`)
- # and ask nameservers, as if top-level domain was queried.
- # This step follows established practice.
- # https://github.com/nameko/nameko/issues/392
- # https://github.com/eventlet/eventlet/issues/363
- if len(qname) == 1:
- step(self._resolver.query, qname.concatenate(dns.name.root),
- rdtype, rdclass, tcp, source, raise_on_no_answer=False)
-
- return end()
-
- def getaliases(self, hostname):
- """Return a list of all the aliases of a given hostname"""
- if self._hosts:
- aliases = self._hosts.getaliases(hostname)
- else:
- aliases = []
- while True:
- try:
- ans = self._resolver.query(hostname, dns.rdatatype.CNAME)
- except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
- break
- else:
- aliases.extend(str(rr.target) for rr in ans.rrset)
- hostname = ans[0].target
- return aliases
-
-
- resolver = ResolverProxy(hosts_resolver=HostsResolver())
-
-
- def resolve(name, family=socket.AF_INET, raises=True, _proxy=None,
- use_network=True):
- """Resolve a name for a given family using the global resolver proxy.
-
- This method is called by the global getaddrinfo() function. If use_network
- is False, only resolution via hosts file will be performed.
-
- Return a dns.resolver.Answer instance. If there is no answer it's
- rrset will be emtpy.
- """
- if family == socket.AF_INET:
- rdtype = dns.rdatatype.A
- elif family == socket.AF_INET6:
- rdtype = dns.rdatatype.AAAA
- else:
- raise socket.gaierror(socket.EAI_FAMILY,
- 'Address family not supported')
-
- if _proxy is None:
- _proxy = resolver
- try:
- try:
- return _proxy.query(name, rdtype, raise_on_no_answer=raises,
- use_network=use_network)
- except dns.resolver.NXDOMAIN:
- if not raises:
- return HostsAnswer(dns.name.Name(name),
- rdtype, dns.rdataclass.IN, None, False)
- raise
- except dns.exception.Timeout:
- raise EAI_EAGAIN_ERROR
- except dns.exception.DNSException:
- raise EAI_NODATA_ERROR
-
-
- def resolve_cname(host):
- """Return the canonical name of a hostname"""
- try:
- ans = resolver.query(host, dns.rdatatype.CNAME)
- except dns.resolver.NoAnswer:
- return host
- except dns.exception.Timeout:
- raise EAI_EAGAIN_ERROR
- except dns.exception.DNSException:
- raise EAI_NODATA_ERROR
- else:
- return str(ans[0].target)
-
-
- def getaliases(host):
- """Return a list of for aliases for the given hostname
-
- This method does translate the dnspython exceptions into
- socket.gaierror exceptions. If no aliases are available an empty
- list will be returned.
- """
- try:
- return resolver.getaliases(host)
- except dns.exception.Timeout:
- raise EAI_EAGAIN_ERROR
- except dns.exception.DNSException:
- raise EAI_NODATA_ERROR
-
-
- def _getaddrinfo_lookup(host, family, flags):
- """Resolve a hostname to a list of addresses
-
- Helper function for getaddrinfo.
- """
- if flags & socket.AI_NUMERICHOST:
- raise EAI_NONAME_ERROR
- addrs = []
- if family == socket.AF_UNSPEC:
- err = None
- for use_network in [False, True]:
- for qfamily in [socket.AF_INET6, socket.AF_INET]:
- try:
- answer = resolve(host, qfamily, False, use_network=use_network)
- except socket.gaierror as e:
- if e.errno not in (socket.EAI_AGAIN, EAI_NONAME_ERROR.errno, EAI_NODATA_ERROR.errno):
- raise
- err = e
- else:
- if answer.rrset:
- addrs.extend(rr.address for rr in answer.rrset)
- if addrs:
- break
- if err is not None and not addrs:
- raise err
- elif family == socket.AF_INET6 and flags & socket.AI_V4MAPPED:
- answer = resolve(host, socket.AF_INET6, False)
- if answer.rrset:
- addrs = [rr.address for rr in answer.rrset]
- if not addrs or flags & socket.AI_ALL:
- answer = resolve(host, socket.AF_INET, False)
- if answer.rrset:
- addrs = ['::ffff:' + rr.address for rr in answer.rrset]
- else:
- answer = resolve(host, family, False)
- if answer.rrset:
- addrs = [rr.address for rr in answer.rrset]
- return str(answer.qname), addrs
-
-
- def getaddrinfo(host, port, family=0, socktype=0, proto=0, flags=0):
- """Replacement for Python's socket.getaddrinfo
-
- This does the A and AAAA lookups asynchronously after which it
- calls the OS' getaddrinfo(3) using the AI_NUMERICHOST flag. This
- flag ensures getaddrinfo(3) does not use the network itself and
- allows us to respect all the other arguments like the native OS.
- """
- if isinstance(host, six.string_types):
- host = host.encode('idna').decode('ascii')
- if host is not None and not is_ip_addr(host):
- qname, addrs = _getaddrinfo_lookup(host, family, flags)
- else:
- qname = host
- addrs = [host]
- aiflags = (flags | socket.AI_NUMERICHOST) & (0xffff ^ socket.AI_CANONNAME)
- res = []
- err = None
- for addr in addrs:
- try:
- ai = socket.getaddrinfo(addr, port, family,
- socktype, proto, aiflags)
- except socket.error as e:
- if flags & socket.AI_ADDRCONFIG:
- err = e
- continue
- raise
- res.extend(ai)
- if not res:
- if err:
- raise err
- raise socket.gaierror(socket.EAI_NONAME, 'No address found')
- if flags & socket.AI_CANONNAME:
- if not is_ip_addr(qname):
- qname = resolve_cname(qname).encode('ascii').decode('idna')
- ai = res[0]
- res[0] = (ai[0], ai[1], ai[2], qname, ai[4])
- return res
-
-
- def gethostbyname(hostname):
- """Replacement for Python's socket.gethostbyname"""
- if is_ipv4_addr(hostname):
- return hostname
- rrset = resolve(hostname)
- return rrset[0].address
-
-
- def gethostbyname_ex(hostname):
- """Replacement for Python's socket.gethostbyname_ex"""
- if is_ipv4_addr(hostname):
- return (hostname, [], [hostname])
- ans = resolve(hostname)
- aliases = getaliases(hostname)
- addrs = [rr.address for rr in ans.rrset]
- qname = str(ans.qname)
- if qname[-1] == '.':
- qname = qname[:-1]
- return (qname, aliases, addrs)
-
-
- def getnameinfo(sockaddr, flags):
- """Replacement for Python's socket.getnameinfo.
-
- Currently only supports IPv4.
- """
- try:
- host, port = sockaddr
- except (ValueError, TypeError):
- if not isinstance(sockaddr, tuple):
- del sockaddr # to pass a stdlib test that is
- # hyper-careful about reference counts
- raise TypeError('getnameinfo() argument 1 must be a tuple')
- else:
- # must be ipv6 sockaddr, pretending we don't know how to resolve it
- raise EAI_NONAME_ERROR
-
- if (flags & socket.NI_NAMEREQD) and (flags & socket.NI_NUMERICHOST):
- # Conflicting flags. Punt.
- raise EAI_NONAME_ERROR
-
- if is_ipv4_addr(host):
- try:
- rrset = resolver.query(
- dns.reversename.from_address(host), dns.rdatatype.PTR)
- if len(rrset) > 1:
- raise socket.error('sockaddr resolved to multiple addresses')
- host = rrset[0].target.to_text(omit_final_dot=True)
- except dns.exception.Timeout:
- if flags & socket.NI_NAMEREQD:
- raise EAI_EAGAIN_ERROR
- except dns.exception.DNSException:
- if flags & socket.NI_NAMEREQD:
- raise EAI_NONAME_ERROR
- else:
- try:
- rrset = resolver.query(host)
- if len(rrset) > 1:
- raise socket.error('sockaddr resolved to multiple addresses')
- if flags & socket.NI_NUMERICHOST:
- host = rrset[0].address
- except dns.exception.Timeout:
- raise EAI_EAGAIN_ERROR
- except dns.exception.DNSException:
- raise socket.gaierror(
- (socket.EAI_NODATA, 'No address associated with hostname'))
-
- if not (flags & socket.NI_NUMERICSERV):
- proto = (flags & socket.NI_DGRAM) and 'udp' or 'tcp'
- port = socket.getservbyport(port, proto)
-
- return (host, port)
-
-
- def _net_read(sock, count, expiration):
- """coro friendly replacement for dns.query._net_read
- Read the specified number of bytes from sock. Keep trying until we
- either get the desired amount, or we hit EOF.
- A Timeout exception will be raised if the operation is not completed
- by the expiration time.
- """
- s = bytearray()
- while count > 0:
- try:
- n = sock.recv(count)
- except socket.timeout:
- # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
- if expiration - time.time() <= 0.0:
- raise dns.exception.Timeout
- eventlet.sleep(0.01)
- continue
- if n == b'':
- raise EOFError
- count = count - len(n)
- s += n
- return s
-
-
- def _net_write(sock, data, expiration):
- """coro friendly replacement for dns.query._net_write
- Write the specified data to the socket.
- A Timeout exception will be raised if the operation is not completed
- by the expiration time.
- """
- current = 0
- l = len(data)
- while current < l:
- try:
- current += sock.send(data[current:])
- except socket.timeout:
- # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
- if expiration - time.time() <= 0.0:
- raise dns.exception.Timeout
-
-
- def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53,
- af=None, source=None, source_port=0, ignore_unexpected=False):
- """coro friendly replacement for dns.query.udp
- Return the response obtained after sending a query via UDP.
-
- @param q: the query
- @type q: dns.message.Message
- @param where: where to send the message
- @type where: string containing an IPv4 or IPv6 address
- @param timeout: The number of seconds to wait before the query times out.
- If None, the default, wait forever.
- @type timeout: float
- @param port: The port to which to send the message. The default is 53.
- @type port: int
- @param af: the address family to use. The default is None, which
- causes the address family to use to be inferred from the form of of where.
- If the inference attempt fails, AF_INET is used.
- @type af: int
- @rtype: dns.message.Message object
- @param source: source address. The default is the IPv4 wildcard address.
- @type source: string
- @param source_port: The port from which to send the message.
- The default is 0.
- @type source_port: int
- @param ignore_unexpected: If True, ignore responses from unexpected
- sources. The default is False.
- @type ignore_unexpected: bool"""
-
- wire = q.to_wire()
- if af is None:
- try:
- af = dns.inet.af_for_address(where)
- except:
- af = dns.inet.AF_INET
- if af == dns.inet.AF_INET:
- destination = (where, port)
- if source is not None:
- source = (source, source_port)
- elif af == dns.inet.AF_INET6:
- # Purge any stray zeroes in source address. When doing the tuple comparison
- # below, we need to always ensure both our target and where we receive replies
- # from are compared with all zeroes removed so that we don't erroneously fail.
- # e.g. ('00::1', 53, 0, 0) != ('::1', 53, 0, 0)
- where_trunc = dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(where))
- destination = (where_trunc, port, 0, 0)
- if source is not None:
- source = (source, source_port, 0, 0)
-
- s = socket.socket(af, socket.SOCK_DGRAM)
- s.settimeout(timeout)
- try:
- expiration = dns.query._compute_expiration(timeout)
- if source is not None:
- s.bind(source)
- while True:
- try:
- s.sendto(wire, destination)
- break
- except socket.timeout:
- # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
- if expiration - time.time() <= 0.0:
- raise dns.exception.Timeout
- eventlet.sleep(0.01)
- continue
-
- tried = False
- while True:
- # If we've tried to receive at least once, check to see if our
- # timer expired
- if tried and (expiration - time.time() <= 0.0):
- raise dns.exception.Timeout
- # Sleep if we are retrying the operation due to a bad source
- # address or a socket timeout.
- if tried:
- eventlet.sleep(0.01)
- tried = True
-
- try:
- (wire, from_address) = s.recvfrom(65535)
- except socket.timeout:
- # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
- continue
- if dns.inet.af_for_address(from_address[0]) == dns.inet.AF_INET6:
- # Purge all possible zeroes for ipv6 to match above logic
- addr = from_address[0]
- addr = dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(addr))
- from_address = (addr, from_address[1], from_address[2], from_address[3])
- if from_address == destination:
- break
- if not ignore_unexpected:
- raise dns.query.UnexpectedSource(
- 'got a response from %s instead of %s'
- % (from_address, destination))
- finally:
- s.close()
-
- r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac)
- if not q.is_response(r):
- raise dns.query.BadResponse()
- return r
-
-
- def tcp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53,
- af=None, source=None, source_port=0):
- """coro friendly replacement for dns.query.tcp
- Return the response obtained after sending a query via TCP.
-
- @param q: the query
- @type q: dns.message.Message object
- @param where: where to send the message
- @type where: string containing an IPv4 or IPv6 address
- @param timeout: The number of seconds to wait before the query times out.
- If None, the default, wait forever.
- @type timeout: float
- @param port: The port to which to send the message. The default is 53.
- @type port: int
- @param af: the address family to use. The default is None, which
- causes the address family to use to be inferred from the form of of where.
- If the inference attempt fails, AF_INET is used.
- @type af: int
- @rtype: dns.message.Message object
- @param source: source address. The default is the IPv4 wildcard address.
- @type source: string
- @param source_port: The port from which to send the message.
- The default is 0.
- @type source_port: int"""
-
- wire = q.to_wire()
- if af is None:
- try:
- af = dns.inet.af_for_address(where)
- except:
- af = dns.inet.AF_INET
- if af == dns.inet.AF_INET:
- destination = (where, port)
- if source is not None:
- source = (source, source_port)
- elif af == dns.inet.AF_INET6:
- destination = (where, port, 0, 0)
- if source is not None:
- source = (source, source_port, 0, 0)
- s = socket.socket(af, socket.SOCK_STREAM)
- s.settimeout(timeout)
- try:
- expiration = dns.query._compute_expiration(timeout)
- if source is not None:
- s.bind(source)
- while True:
- try:
- s.connect(destination)
- break
- except socket.timeout:
- # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
- if expiration - time.time() <= 0.0:
- raise dns.exception.Timeout
- eventlet.sleep(0.01)
- continue
-
- l = len(wire)
- # copying the wire into tcpmsg is inefficient, but lets us
- # avoid writev() or doing a short write that would get pushed
- # onto the net
- tcpmsg = struct.pack("!H", l) + wire
- _net_write(s, tcpmsg, expiration)
- ldata = _net_read(s, 2, expiration)
- (l,) = struct.unpack("!H", ldata)
- wire = bytes(_net_read(s, l, expiration))
- finally:
- s.close()
- r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac)
- if not q.is_response(r):
- raise dns.query.BadResponse()
- return r
-
-
- def reset():
- resolver.clear()
-
- # Install our coro-friendly replacements for the tcp and udp query methods.
- dns.query.tcp = tcp
- dns.query.udp = udp
|