1 '''greendns - non-blocking DNS support for Eventlet
4 # Portions of this code taken from the gogreen project:
5 # http://github.com/slideinc/gogreen
7 # Copyright (c) 2005-2010 Slide, Inc.
10 # Redistribution and use in source and binary forms, with or without
11 # modification, are permitted provided that the following conditions are
14 # * Redistributions of source code must retain the above copyright
15 # notice, this list of conditions and the following disclaimer.
16 # * Redistributions in binary form must reproduce the above
17 # copyright notice, this list of conditions and the following
18 # disclaimer in the documentation and/or other materials provided
19 # with the distribution.
20 # * Neither the name of the author nor the names of other
21 # contributors may be used to endorse or promote products derived
22 # from this software without specific prior written permission.
24 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 from eventlet import patcher
38 from eventlet.green import _socket_nodns
39 from eventlet.green import os
40 from eventlet.green import time
41 from eventlet.green import select
42 from eventlet.support import six
45 dns = patcher.import_patched('dns',
50 dns.resolver = patcher.import_patched('dns.resolver',
56 for pkg in ('dns.entropy', 'dns.inet', 'dns.query'):
57 setattr(dns, pkg.split('.')[1], patcher.import_patched(pkg,
61 socket=_socket_nodns))
63 for pkg in ['dns.rdtypes.IN', 'dns.rdtypes.ANY']:
64 setattr(dns.rdtypes, pkg.split('.')[-1], patcher.import_patched(pkg))
65 for pkg in ['dns.rdtypes.IN.A', 'dns.rdtypes.IN.AAAA']:
66 setattr(dns.rdtypes.IN, pkg.split('.')[-1], patcher.import_patched(pkg))
67 for pkg in ['dns.rdtypes.ANY.CNAME']:
68 setattr(dns.rdtypes.ANY, pkg.split('.')[-1], patcher.import_patched(pkg))
71 socket = _socket_nodns
73 DNS_QUERY_TIMEOUT = 10.0
76 EAI_EAGAIN_ERROR = socket.gaierror(socket.EAI_AGAIN, 'Lookup timed out')
77 EAI_NODATA_ERROR = socket.gaierror(socket.EAI_NODATA, 'No address associated with hostname')
78 EAI_NONAME_ERROR = socket.gaierror(socket.EAI_NONAME, 'Name or service not known')
81 def is_ipv4_addr(host):
82 """Return True if host is a valid IPv4 address"""
83 if not isinstance(host, six.string_types):
86 dns.ipv4.inet_aton(host)
87 except dns.exception.SyntaxError:
93 def is_ipv6_addr(host):
94 """Return True if host is a valid IPv6 address"""
95 if not isinstance(host, six.string_types):
98 dns.ipv6.inet_aton(host)
99 except dns.exception.SyntaxError:
105 def is_ip_addr(host):
106 """Return True if host is a valid IPv4 or IPv6 address"""
107 return is_ipv4_addr(host) or is_ipv6_addr(host)
110 class HostsAnswer(dns.resolver.Answer):
111 """Answer class for HostsResolver object"""
113 def __init__(self, qname, rdtype, rdclass, rrset, raise_on_no_answer=True):
114 """Create a new answer
116 :qname: A dns.name.Name instance of the query name
117 :rdtype: The rdatatype of the query
118 :rdclass: The rdataclass of the query
119 :rrset: The dns.rrset.RRset with the response, must have ttl attribute
120 :raise_on_no_answer: Whether to raise dns.resolver.NoAnswer if no
126 self.rdclass = rdclass
127 self.canonical_name = qname
128 if not rrset and raise_on_no_answer:
129 raise dns.resolver.NoAnswer()
131 self.expiration = (time.time() +
132 rrset.ttl if hasattr(rrset, 'ttl') else 0)
135 class HostsResolver(object):
136 """Class to parse the hosts file
141 :fname: The filename of the hosts file in use.
142 :interval: The time between checking for hosts file modification
145 def __init__(self, fname=None, interval=HOSTS_TTL):
146 self._v4 = {} # name -> ipv4
147 self._v6 = {} # name -> ipv6
148 self._aliases = {} # name -> cannonical_name
149 self.interval = interval
152 if os.name == 'posix':
153 self.fname = '/etc/hosts'
154 elif os.name == 'nt':
155 self.fname = os.path.expandvars(
156 r'%SystemRoot%\system32\drivers\etc\hosts')
161 def _readlines(self):
162 """Read the contents of the hosts file
164 Return list of lines, comment lines and empty lines are
167 Note that this performs disk I/O so can be blocking.
171 with open(self.fname, 'rU') as fp:
174 if line and line[0] != '#':
176 except (IOError, OSError):
183 This will unconditionally (re)load the data from the hosts
186 lines = self._readlines()
189 self._aliases.clear()
197 elif is_ipv6_addr(ip):
198 if ip.startswith('fe80'):
199 # Do not use link-local addresses, OSX stores these here
208 self._aliases[alias] = cname
209 self._last_load = time.time()
211 def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,
212 tcp=False, source=None, raise_on_no_answer=True):
213 """Query the hosts file
215 The known rdtypes are dns.rdatatype.A, dns.rdatatype.AAAA and
218 The ``rdclass`` parameter must be dns.rdataclass.IN while the
219 ``tcp`` and ``source`` parameters are ignored.
221 Return a HostAnswer instance or raise a dns.resolver.NoAnswer
225 if self._last_load + self.interval < now:
227 rdclass = dns.rdataclass.IN
228 if isinstance(qname, six.string_types):
230 qname = dns.name.from_text(qname)
233 rrset = dns.rrset.RRset(qname, rdclass, rdtype)
234 rrset.ttl = self._last_load + self.interval - now
235 if rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.A:
236 addr = self._v4.get(name)
237 if not addr and qname.is_absolute():
238 addr = self._v4.get(name[:-1])
240 rrset.add(dns.rdtypes.IN.A.A(rdclass, rdtype, addr))
241 elif rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.AAAA:
242 addr = self._v6.get(name)
243 if not addr and qname.is_absolute():
244 addr = self._v6.get(name[:-1])
246 rrset.add(dns.rdtypes.IN.AAAA.AAAA(rdclass, rdtype, addr))
247 elif rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.CNAME:
248 cname = self._aliases.get(name)
249 if not cname and qname.is_absolute():
250 cname = self._aliases.get(name[:-1])
252 rrset.add(dns.rdtypes.ANY.CNAME.CNAME(
253 rdclass, rdtype, dns.name.from_text(cname)))
254 return HostsAnswer(qname, rdtype, rdclass, rrset, raise_on_no_answer)
256 def getaliases(self, hostname):
257 """Return a list of all the aliases of a given cname"""
258 # Due to the way store aliases this is a bit inefficient, this
259 # clearly was an afterthought. But this is only used by
260 # gethostbyname_ex so it's probably fine.
262 if hostname in self._aliases:
263 cannon = self._aliases[hostname]
266 aliases.append(cannon)
267 for alias, cname in six.iteritems(self._aliases):
269 aliases.append(alias)
270 aliases.remove(hostname)
274 class ResolverProxy(object):
275 """Resolver class which can also use /etc/hosts
277 Initialise with a HostsResolver instance in order for it to also
281 def __init__(self, hosts_resolver=None, filename='/etc/resolv.conf'):
282 """Initialise the resolver proxy
284 :param hosts_resolver: An instance of HostsResolver to use.
286 :param filename: The filename containing the resolver
287 configuration. The default value is correct for both UNIX
288 and Windows, on Windows it will result in the configuration
289 being read from the Windows registry.
291 self._hosts = hosts_resolver
292 self._filename = filename
293 self._resolver = dns.resolver.Resolver(filename=self._filename)
294 self._resolver.cache = dns.resolver.LRUCache()
297 self._resolver = dns.resolver.Resolver(filename=self._filename)
298 self._resolver.cache = dns.resolver.Cache()
300 def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN,
301 tcp=False, source=None, raise_on_no_answer=True):
302 """Query the resolver, using /etc/hosts if enabled"""
305 if rdclass == dns.rdataclass.IN and self._hosts:
307 return self._hosts.query(qname, rdtype)
308 except dns.resolver.NoAnswer:
310 return self._resolver.query(qname, rdtype, rdclass,
311 tcp, source, raise_on_no_answer)
313 def getaliases(self, hostname):
314 """Return a list of all the aliases of a given hostname"""
316 aliases = self._hosts.getaliases(hostname)
321 ans = self._resolver.query(hostname, dns.rdatatype.CNAME)
322 except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
325 aliases.extend(str(rr.target) for rr in ans.rrset)
326 hostname = ans[0].target
330 resolver = ResolverProxy(hosts_resolver=HostsResolver())
333 def resolve(name, family=socket.AF_INET, raises=True):
334 """Resolve a name for a given family using the global resolver proxy
336 This method is called by the global getaddrinfo() function.
338 Return a dns.resolver.Answer instance. If there is no answer it's
341 if family == socket.AF_INET:
342 rdtype = dns.rdatatype.A
343 elif family == socket.AF_INET6:
344 rdtype = dns.rdatatype.AAAA
346 raise socket.gaierror(socket.EAI_FAMILY,
347 'Address family not supported')
350 return resolver.query(name, rdtype, raise_on_no_answer=raises)
351 except dns.resolver.NXDOMAIN:
353 return HostsAnswer(dns.name.Name(name),
354 rdtype, dns.rdataclass.IN, None, False)
356 except dns.exception.Timeout:
357 raise EAI_EAGAIN_ERROR
358 except dns.exception.DNSException:
359 raise EAI_NODATA_ERROR
362 def resolve_cname(host):
363 """Return the canonical name of a hostname"""
365 ans = resolver.query(host, dns.rdatatype.CNAME)
366 except dns.resolver.NoAnswer:
368 except dns.exception.Timeout:
369 raise EAI_EAGAIN_ERROR
370 except dns.exception.DNSException:
371 raise EAI_NODATA_ERROR
373 return str(ans[0].target)
376 def getaliases(host):
377 """Return a list of for aliases for the given hostname
379 This method does translate the dnspython exceptions into
380 socket.gaierror exceptions. If no aliases are available an empty
381 list will be returned.
384 return resolver.getaliases(host)
385 except dns.exception.Timeout:
386 raise EAI_EAGAIN_ERROR
387 except dns.exception.DNSException:
388 raise EAI_NODATA_ERROR
391 def _getaddrinfo_lookup(host, family, flags):
392 """Resolve a hostname to a list of addresses
394 Helper function for getaddrinfo.
396 if flags & socket.AI_NUMERICHOST:
397 raise EAI_NONAME_ERROR
399 if family == socket.AF_UNSPEC:
400 for qfamily in [socket.AF_INET6, socket.AF_INET]:
401 answer = resolve(host, qfamily, False)
403 addrs.extend([rr.address for rr in answer.rrset])
404 elif family == socket.AF_INET6 and flags & socket.AI_V4MAPPED:
405 answer = resolve(host, socket.AF_INET6, False)
407 addrs = [rr.address for rr in answer.rrset]
408 if not addrs or flags & socket.AI_ALL:
409 answer = resolve(host, socket.AF_INET, False)
411 addrs = ['::ffff:' + rr.address for rr in answer.rrset]
413 answer = resolve(host, family, False)
415 addrs = [rr.address for rr in answer.rrset]
416 return str(answer.qname), addrs
419 def getaddrinfo(host, port, family=0, socktype=0, proto=0, flags=0):
420 """Replacement for Python's socket.getaddrinfo
422 This does the A and AAAA lookups asynchronously after which it
423 calls the OS' getaddrinfo(3) using the AI_NUMERICHOST flag. This
424 flag ensures getaddrinfo(3) does not use the network itself and
425 allows us to respect all the other arguments like the native OS.
427 if isinstance(host, six.string_types):
428 host = host.encode('idna').decode('ascii')
429 if host is not None and not is_ip_addr(host):
430 qname, addrs = _getaddrinfo_lookup(host, family, flags)
434 aiflags = (flags | socket.AI_NUMERICHOST) & (0xffff ^ socket.AI_CANONNAME)
439 ai = socket.getaddrinfo(addr, port, family,
440 socktype, proto, aiflags)
441 except socket.error as e:
442 if flags & socket.AI_ADDRCONFIG:
450 raise socket.gaierror(socket.EAI_NONAME, 'No address found')
451 if flags & socket.AI_CANONNAME:
452 if not is_ip_addr(qname):
453 qname = resolve_cname(qname).encode('ascii').decode('idna')
455 res[0] = (ai[0], ai[1], ai[2], qname, ai[4])
459 def gethostbyname(hostname):
460 """Replacement for Python's socket.gethostbyname"""
461 if is_ipv4_addr(hostname):
463 rrset = resolve(hostname)
464 return rrset[0].address
467 def gethostbyname_ex(hostname):
468 """Replacement for Python's socket.gethostbyname_ex"""
469 if is_ipv4_addr(hostname):
470 return (hostname, [], [hostname])
471 ans = resolve(hostname)
472 aliases = getaliases(hostname)
473 addrs = [rr.address for rr in ans.rrset]
474 qname = str(ans.qname)
477 return (qname, aliases, addrs)
480 def getnameinfo(sockaddr, flags):
481 """Replacement for Python's socket.getnameinfo.
483 Currently only supports IPv4.
486 host, port = sockaddr
487 except (ValueError, TypeError):
488 if not isinstance(sockaddr, tuple):
489 del sockaddr # to pass a stdlib test that is
490 # hyper-careful about reference counts
491 raise TypeError('getnameinfo() argument 1 must be a tuple')
493 # must be ipv6 sockaddr, pretending we don't know how to resolve it
494 raise EAI_NONAME_ERROR
496 if (flags & socket.NI_NAMEREQD) and (flags & socket.NI_NUMERICHOST):
497 # Conflicting flags. Punt.
498 raise EAI_NONAME_ERROR
500 if is_ipv4_addr(host):
502 rrset = resolver.query(
503 dns.reversename.from_address(host), dns.rdatatype.PTR)
505 raise socket.error('sockaddr resolved to multiple addresses')
506 host = rrset[0].target.to_text(omit_final_dot=True)
507 except dns.exception.Timeout:
508 if flags & socket.NI_NAMEREQD:
509 raise EAI_EAGAIN_ERROR
510 except dns.exception.DNSException:
511 if flags & socket.NI_NAMEREQD:
512 raise EAI_NONAME_ERROR
515 rrset = resolver.query(host)
517 raise socket.error('sockaddr resolved to multiple addresses')
518 if flags & socket.NI_NUMERICHOST:
519 host = rrset[0].address
520 except dns.exception.Timeout:
521 raise EAI_EAGAIN_ERROR
522 except dns.exception.DNSException:
523 raise socket.gaierror(
524 (socket.EAI_NODATA, 'No address associated with hostname'))
526 if not (flags & socket.NI_NUMERICSERV):
527 proto = (flags & socket.NI_DGRAM) and 'udp' or 'tcp'
528 port = socket.getservbyport(port, proto)
533 def _net_read(sock, count, expiration):
534 """coro friendly replacement for dns.query._net_write
535 Read the specified number of bytes from sock. Keep trying until we
536 either get the desired amount, or we hit EOF.
537 A Timeout exception will be raised if the operation is not completed
538 by the expiration time.
544 except socket.timeout:
545 # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
546 if expiration - time.time() <= 0.0:
547 raise dns.exception.Timeout
550 count = count - len(n)
555 def _net_write(sock, data, expiration):
556 """coro friendly replacement for dns.query._net_write
557 Write the specified data to the socket.
558 A Timeout exception will be raised if the operation is not completed
559 by the expiration time.
565 current += sock.send(data[current:])
566 except socket.timeout:
567 # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
568 if expiration - time.time() <= 0.0:
569 raise dns.exception.Timeout
572 def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53,
573 af=None, source=None, source_port=0, ignore_unexpected=False):
574 """coro friendly replacement for dns.query.udp
575 Return the response obtained after sending a query via UDP.
578 @type q: dns.message.Message
579 @param where: where to send the message
580 @type where: string containing an IPv4 or IPv6 address
581 @param timeout: The number of seconds to wait before the query times out.
582 If None, the default, wait forever.
584 @param port: The port to which to send the message. The default is 53.
586 @param af: the address family to use. The default is None, which
587 causes the address family to use to be inferred from the form of of where.
588 If the inference attempt fails, AF_INET is used.
590 @rtype: dns.message.Message object
591 @param source: source address. The default is the IPv4 wildcard address.
593 @param source_port: The port from which to send the message.
595 @type source_port: int
596 @param ignore_unexpected: If True, ignore responses from unexpected
597 sources. The default is False.
598 @type ignore_unexpected: bool"""
603 af = dns.inet.af_for_address(where)
605 af = dns.inet.AF_INET
606 if af == dns.inet.AF_INET:
607 destination = (where, port)
608 if source is not None:
609 source = (source, source_port)
610 elif af == dns.inet.AF_INET6:
611 destination = (where, port, 0, 0)
612 if source is not None:
613 source = (source, source_port, 0, 0)
615 s = socket.socket(af, socket.SOCK_DGRAM)
616 s.settimeout(timeout)
618 expiration = dns.query._compute_expiration(timeout)
619 if source is not None:
622 s.sendto(wire, destination)
623 except socket.timeout:
624 # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
625 if expiration - time.time() <= 0.0:
626 raise dns.exception.Timeout
629 (wire, from_address) = s.recvfrom(65535)
630 except socket.timeout:
631 # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
632 if expiration - time.time() <= 0.0:
633 raise dns.exception.Timeout
634 if from_address == destination:
636 if not ignore_unexpected:
637 raise dns.query.UnexpectedSource(
638 'got a response from %s instead of %s'
639 % (from_address, destination))
643 r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac)
644 if not q.is_response(r):
645 raise dns.query.BadResponse()
649 def tcp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53,
650 af=None, source=None, source_port=0):
651 """coro friendly replacement for dns.query.tcp
652 Return the response obtained after sending a query via TCP.
655 @type q: dns.message.Message object
656 @param where: where to send the message
657 @type where: string containing an IPv4 or IPv6 address
658 @param timeout: The number of seconds to wait before the query times out.
659 If None, the default, wait forever.
661 @param port: The port to which to send the message. The default is 53.
663 @param af: the address family to use. The default is None, which
664 causes the address family to use to be inferred from the form of of where.
665 If the inference attempt fails, AF_INET is used.
667 @rtype: dns.message.Message object
668 @param source: source address. The default is the IPv4 wildcard address.
670 @param source_port: The port from which to send the message.
672 @type source_port: int"""
677 af = dns.inet.af_for_address(where)
679 af = dns.inet.AF_INET
680 if af == dns.inet.AF_INET:
681 destination = (where, port)
682 if source is not None:
683 source = (source, source_port)
684 elif af == dns.inet.AF_INET6:
685 destination = (where, port, 0, 0)
686 if source is not None:
687 source = (source, source_port, 0, 0)
688 s = socket.socket(af, socket.SOCK_STREAM)
689 s.settimeout(timeout)
691 expiration = dns.query._compute_expiration(timeout)
692 if source is not None:
695 s.connect(destination)
696 except socket.timeout:
697 # Q: Do we also need to catch coro.CoroutineSocketWake and pass?
698 if expiration - time.time() <= 0.0:
699 raise dns.exception.Timeout
702 # copying the wire into tcpmsg is inefficient, but lets us
703 # avoid writev() or doing a short write that would get pushed
705 tcpmsg = struct.pack("!H", l) + wire
706 _net_write(s, tcpmsg, expiration)
707 ldata = _net_read(s, 2, expiration)
708 (l,) = struct.unpack("!H", ldata)
709 wire = _net_read(s, l, expiration)
712 r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac)
713 if not q.is_response(r):
714 raise dns.query.BadResponse()
721 # Install our coro-friendly replacements for the tcp and udp query methods.