diff options
Diffstat (limited to 'python/gevent/resolver/ares.py')
-rw-r--r-- | python/gevent/resolver/ares.py | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/python/gevent/resolver/ares.py b/python/gevent/resolver/ares.py new file mode 100644 index 0000000..ea6e919 --- /dev/null +++ b/python/gevent/resolver/ares.py @@ -0,0 +1,357 @@ +# Copyright (c) 2011-2015 Denis Bilenko. See LICENSE for details. +""" +c-ares based hostname resolver. +""" +from __future__ import absolute_import, print_function, division +import os +import sys + +from _socket import getaddrinfo +from _socket import gaierror +from _socket import error + +from gevent._compat import string_types +from gevent._compat import text_type + +from gevent._compat import reraise +from gevent._compat import PY3 + +from gevent.hub import Waiter +from gevent.hub import get_hub + +from gevent.socket import AF_UNSPEC +from gevent.socket import AF_INET +from gevent.socket import AF_INET6 +from gevent.socket import SOCK_STREAM +from gevent.socket import SOCK_DGRAM +from gevent.socket import SOCK_RAW +from gevent.socket import AI_NUMERICHOST + +from gevent._config import config +from gevent._config import AresSettingMixin + +from .cares import channel, InvalidIP # pylint:disable=import-error,no-name-in-module +from . import _lookup_port as lookup_port +from . import _resolve_special +from . import AbstractResolver + +__all__ = ['Resolver'] + + +class Resolver(AbstractResolver): + """ + Implementation of the resolver API using the `c-ares`_ library. + + This implementation uses the c-ares library to handle name + resolution. c-ares is natively asynchronous at the socket level + and so integrates well into gevent's event loop. + + In comparison to :class:`gevent.resolver_thread.Resolver` (which + delegates to the native system resolver), the implementation is + much more complex. In addition, there have been reports of it not + properly honoring certain system configurations (for example, the + order in which IPv4 and IPv6 results are returned may not match + the threaded resolver). However, because it does not use threads, + it may scale better for applications that make many lookups. + + There are some known differences from the system resolver: + + - ``gethostbyname_ex`` and ``gethostbyaddr`` may return different + for the ``aliaslist`` tuple member. (Sometimes the same, + sometimes in a different order, sometimes a different alias + altogether.) + - ``gethostbyname_ex`` may return the ``ipaddrlist`` in a different order. + - ``getaddrinfo`` does not return ``SOCK_RAW`` results. + - ``getaddrinfo`` may return results in a different order. + - Handling of ``.local`` (mDNS) names may be different, even if they are listed in + the hosts file. + - c-ares will not resolve ``broadcasthost``, even if listed in the hosts file. + - This implementation may raise ``gaierror(4)`` where the system implementation would raise + ``herror(1)``. + - The results for ``localhost`` may be different. In particular, some system + resolvers will return more results from ``getaddrinfo`` than c-ares does, + such as SOCK_DGRAM results, and c-ares may report more ips on a multi-homed + host. + + .. caution:: This module is considered extremely experimental on PyPy, and + due to its implementation in cython, it may be slower. It may also lead to + interpreter crashes. + + .. _c-ares: http://c-ares.haxx.se + """ + + ares_class = channel + + def __init__(self, hub=None, use_environ=True, **kwargs): + if hub is None: + hub = get_hub() + self.hub = hub + if use_environ: + for setting in config.settings.values(): + if isinstance(setting, AresSettingMixin): + value = setting.get() + if value is not None: + kwargs.setdefault(setting.kwarg_name, value) + self.ares = self.ares_class(hub.loop, **kwargs) + self.pid = os.getpid() + self.params = kwargs + self.fork_watcher = hub.loop.fork(ref=False) + self.fork_watcher.start(self._on_fork) + + def __repr__(self): + return '<gevent.resolver_ares.Resolver at 0x%x ares=%r>' % (id(self), self.ares) + + def _on_fork(self): + # NOTE: See comment in gevent.hub.reinit. + pid = os.getpid() + if pid != self.pid: + self.hub.loop.run_callback(self.ares.destroy) + self.ares = self.ares_class(self.hub.loop, **self.params) + self.pid = pid + + def close(self): + if self.ares is not None: + self.hub.loop.run_callback(self.ares.destroy) + self.ares = None + self.fork_watcher.stop() + + def gethostbyname(self, hostname, family=AF_INET): + hostname = _resolve_special(hostname, family) + return self.gethostbyname_ex(hostname, family)[-1][0] + + def gethostbyname_ex(self, hostname, family=AF_INET): + if PY3: + if isinstance(hostname, str): + hostname = hostname.encode('idna') + elif not isinstance(hostname, (bytes, bytearray)): + raise TypeError('Expected es(idna), not %s' % type(hostname).__name__) + else: + if isinstance(hostname, text_type): + hostname = hostname.encode('ascii') + elif not isinstance(hostname, str): + raise TypeError('Expected string, not %s' % type(hostname).__name__) + + while True: + ares = self.ares + try: + waiter = Waiter(self.hub) + ares.gethostbyname(waiter, hostname, family) + result = waiter.get() + if not result[-1]: + raise gaierror(-5, 'No address associated with hostname') + return result + except gaierror: + if ares is self.ares: + if hostname == b'255.255.255.255': + # The stdlib handles this case in 2.7 and 3.x, but ares does not. + # It is tested by test_socket.py in 3.4. + # HACK: So hardcode the expected return. + return ('255.255.255.255', [], ['255.255.255.255']) + raise + # "self.ares is not ares" means channel was destroyed (because we were forked) + + def _lookup_port(self, port, socktype): + return lookup_port(port, socktype) + + def _getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): + # pylint:disable=too-many-locals,too-many-branches + if isinstance(host, text_type): + host = host.encode('idna') + elif not isinstance(host, str) or (flags & AI_NUMERICHOST): + # this handles cases which do not require network access + # 1) host is None + # 2) host is of an invalid type + # 3) AI_NUMERICHOST flag is set + return getaddrinfo(host, port, family, socktype, proto, flags) + # we also call _socket.getaddrinfo below if family is not one of AF_* + + port, socktypes = self._lookup_port(port, socktype) + + socktype_proto = [(SOCK_STREAM, 6), (SOCK_DGRAM, 17), (SOCK_RAW, 0)] + if socktypes: + socktype_proto = [(x, y) for (x, y) in socktype_proto if x in socktypes] + if proto: + socktype_proto = [(x, y) for (x, y) in socktype_proto if proto == y] + + ares = self.ares + + if family == AF_UNSPEC: + ares_values = Values(self.hub, 2) + ares.gethostbyname(ares_values, host, AF_INET) + ares.gethostbyname(ares_values, host, AF_INET6) + elif family == AF_INET: + ares_values = Values(self.hub, 1) + ares.gethostbyname(ares_values, host, AF_INET) + elif family == AF_INET6: + ares_values = Values(self.hub, 1) + ares.gethostbyname(ares_values, host, AF_INET6) + else: + raise gaierror(5, 'ai_family not supported: %r' % (family, )) + + values = ares_values.get() + if len(values) == 2 and values[0] == values[1]: + values.pop() + + result = [] + result4 = [] + result6 = [] + + for addrs in values: + if addrs.family == AF_INET: + for addr in addrs[-1]: + sockaddr = (addr, port) + for socktype4, proto4 in socktype_proto: + result4.append((AF_INET, socktype4, proto4, '', sockaddr)) + elif addrs.family == AF_INET6: + for addr in addrs[-1]: + if addr == '::1': + dest = result + else: + dest = result6 + sockaddr = (addr, port, 0, 0) + for socktype6, proto6 in socktype_proto: + dest.append((AF_INET6, socktype6, proto6, '', sockaddr)) + + # As of 2016, some platforms return IPV6 first and some do IPV4 first, + # and some might even allow configuration of which is which. For backwards + # compatibility with earlier releases (but not necessarily resolver_thread!) + # we return 4 first. See https://github.com/gevent/gevent/issues/815 for more. + result += result4 + result6 + + if not result: + raise gaierror(-5, 'No address associated with hostname') + + return result + + def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): + while True: + ares = self.ares + try: + return self._getaddrinfo(host, port, family, socktype, proto, flags) + except gaierror: + if ares is self.ares: + raise + + def _gethostbyaddr(self, ip_address): + if PY3: + if isinstance(ip_address, str): + ip_address = ip_address.encode('idna') + elif not isinstance(ip_address, (bytes, bytearray)): + raise TypeError('Expected es(idna), not %s' % type(ip_address).__name__) + else: + if isinstance(ip_address, text_type): + ip_address = ip_address.encode('ascii') + elif not isinstance(ip_address, str): + raise TypeError('Expected string, not %s' % type(ip_address).__name__) + + waiter = Waiter(self.hub) + try: + self.ares.gethostbyaddr(waiter, ip_address) + return waiter.get() + except InvalidIP: + result = self._getaddrinfo(ip_address, None, family=AF_UNSPEC, socktype=SOCK_DGRAM) + if not result: + raise + _ip_address = result[0][-1][0] + if isinstance(_ip_address, text_type): + _ip_address = _ip_address.encode('ascii') + if _ip_address == ip_address: + raise + waiter.clear() + self.ares.gethostbyaddr(waiter, _ip_address) + return waiter.get() + + def gethostbyaddr(self, ip_address): + ip_address = _resolve_special(ip_address, AF_UNSPEC) + while True: + ares = self.ares + try: + return self._gethostbyaddr(ip_address) + except gaierror: + if ares is self.ares: + raise + + def _getnameinfo(self, sockaddr, flags): + if not isinstance(flags, int): + raise TypeError('an integer is required') + if not isinstance(sockaddr, tuple): + raise TypeError('getnameinfo() argument 1 must be a tuple') + + address = sockaddr[0] + if not PY3 and isinstance(address, text_type): + address = address.encode('ascii') + + if not isinstance(address, string_types): + raise TypeError('sockaddr[0] must be a string, not %s' % type(address).__name__) + + port = sockaddr[1] + if not isinstance(port, int): + raise TypeError('port must be an integer, not %s' % type(port)) + + waiter = Waiter(self.hub) + result = self._getaddrinfo(address, str(sockaddr[1]), family=AF_UNSPEC, socktype=SOCK_DGRAM) + if not result: + reraise(*sys.exc_info()) + elif len(result) != 1: + raise error('sockaddr resolved to multiple addresses') + family, _socktype, _proto, _name, address = result[0] + + if family == AF_INET: + if len(sockaddr) != 2: + raise error("IPv4 sockaddr must be 2 tuple") + elif family == AF_INET6: + address = address[:2] + sockaddr[2:] + + self.ares.getnameinfo(waiter, address, flags) + node, service = waiter.get() + + if service is None: + if PY3: + # ares docs: "If the query did not complete + # successfully, or one of the values was not + # requested, node or service will be NULL ". Python 2 + # allows that for the service, but Python 3 raises + # an error. This is tested by test_socket in py 3.4 + err = gaierror('nodename nor servname provided, or not known') + err.errno = 8 + raise err + service = '0' + return node, service + + def getnameinfo(self, sockaddr, flags): + while True: + ares = self.ares + try: + return self._getnameinfo(sockaddr, flags) + except gaierror: + if ares is self.ares: + raise + + +class Values(object): + # helper to collect multiple values; ignore errors unless nothing has succeeded + # QQQ could probably be moved somewhere - hub.py? + + __slots__ = ['count', 'values', 'error', 'waiter'] + + def __init__(self, hub, count): + self.count = count + self.values = [] + self.error = None + self.waiter = Waiter(hub) + + def __call__(self, source): + self.count -= 1 + if source.exception is None: + self.values.append(source.value) + else: + self.error = source.exception + if self.count <= 0: + self.waiter.switch(None) + + def get(self): + self.waiter.get() + if self.values: + return self.values + assert error is not None + raise self.error # pylint:disable=raising-bad-type |