# Copyright (c) 2018 gevent contributors. See LICENSE for details. import _socket from _socket import AF_INET from _socket import AF_UNSPEC from _socket import AI_CANONNAME from _socket import AI_PASSIVE from _socket import AI_NUMERICHOST from _socket import EAI_NONAME from _socket import EAI_SERVICE from _socket import SOCK_DGRAM from _socket import SOCK_STREAM from _socket import SOL_TCP from _socket import error from _socket import gaierror from _socket import getaddrinfo as native_getaddrinfo from _socket import getnameinfo as native_getnameinfo from _socket import gethostbyaddr as native_gethostbyaddr from _socket import gethostbyname as native_gethostbyname from _socket import gethostbyname_ex as native_gethostbyname_ex from _socket import getservbyname as native_getservbyname from gevent._compat import string_types from gevent._compat import text_type from gevent._compat import hostname_types from gevent._compat import integer_types from gevent._compat import PY3 from gevent._compat import PYPY from gevent._compat import MAC from gevent.resolver._addresses import is_ipv6_addr # Nothing public here. __all__ = () # trigger import of encodings.idna to avoid https://github.com/gevent/gevent/issues/349 u'foo'.encode('idna') def _lookup_port(port, socktype): # pylint:disable=too-many-branches socktypes = [] if isinstance(port, string_types): try: port = int(port) except ValueError: try: if socktype == 0: origport = port try: port = native_getservbyname(port, 'tcp') socktypes.append(SOCK_STREAM) except error: port = native_getservbyname(port, 'udp') socktypes.append(SOCK_DGRAM) else: try: if port == native_getservbyname(origport, 'udp'): socktypes.append(SOCK_DGRAM) except error: pass elif socktype == SOCK_STREAM: port = native_getservbyname(port, 'tcp') elif socktype == SOCK_DGRAM: port = native_getservbyname(port, 'udp') else: raise gaierror(EAI_SERVICE, 'Servname not supported for ai_socktype') except error as ex: if 'not found' in str(ex): raise gaierror(EAI_SERVICE, 'Servname not supported for ai_socktype') raise gaierror(str(ex)) except UnicodeEncodeError: raise error('Int or String expected', port) elif port is None: port = 0 elif isinstance(port, integer_types): pass else: raise error('Int or String expected', port, type(port)) port = int(port % 65536) if not socktypes and socktype: socktypes.append(socktype) return port, socktypes def _resolve_special(hostname, family): if not isinstance(hostname, hostname_types): raise TypeError("argument 1 must be str, bytes or bytearray, not %s" % (type(hostname),)) if hostname in (u'', b''): result = native_getaddrinfo(None, 0, family, SOCK_DGRAM, 0, AI_PASSIVE) if len(result) != 1: raise error('wildcard resolved to multiple address') return result[0][4][0] return hostname class AbstractResolver(object): HOSTNAME_ENCODING = 'idna' if PY3 else 'ascii' _LOCAL_HOSTNAMES = ( b'localhost', b'ip6-localhost', b'::1', b'127.0.0.1', ) _LOCAL_AND_BROADCAST_HOSTNAMES = _LOCAL_HOSTNAMES + ( b'255.255.255.255', b'', ) EAI_NONAME_MSG = ( 'nodename nor servname provided, or not known' if MAC else 'Name or service not known' ) EAI_FAMILY_MSG = ( 'ai_family not supported' ) _KNOWN_ADDR_FAMILIES = { v for k, v in vars(_socket).items() if k.startswith('AF_') } _KNOWN_SOCKTYPES = { v for k, v in vars(_socket).items() if k.startswith('SOCK_') and k not in ('SOCK_CLOEXEC', 'SOCK_MAX_SIZE') } @staticmethod def fixup_gaierror(func): import functools @functools.wraps(func) def resolve(self, *args, **kwargs): try: return func(self, *args, **kwargs) except gaierror as ex: if ex.args[0] == EAI_NONAME and len(ex.args) == 1: # dnspython doesn't set an error message ex.args = (EAI_NONAME, self.EAI_NONAME_MSG) ex.errno = EAI_NONAME raise return resolve def _hostname_to_bytes(self, hostname): if isinstance(hostname, text_type): hostname = hostname.encode(self.HOSTNAME_ENCODING) elif not isinstance(hostname, (bytes, bytearray)): raise TypeError('Expected str, bytes or bytearray, not %s' % type(hostname).__name__) return bytes(hostname) def gethostbyname(self, hostname, family=AF_INET): # The native ``gethostbyname`` and ``gethostbyname_ex`` have some different # behaviour with special names. Notably, ``gethostbyname`` will handle # both "" and "255.255.255.255", while ``gethostbyname_ex`` refuses to # handle those; they result in different errors, too. So we can't # pass those throgh. hostname = self._hostname_to_bytes(hostname) if hostname in self._LOCAL_AND_BROADCAST_HOSTNAMES: return native_gethostbyname(hostname) hostname = _resolve_special(hostname, family) return self.gethostbyname_ex(hostname, family)[-1][0] def _gethostbyname_ex(self, hostname_bytes, family): """Raise an ``herror`` or a ``gaierror``.""" aliases = self._getaliases(hostname_bytes, family) addresses = [] tuples = self.getaddrinfo(hostname_bytes, 0, family, SOCK_STREAM, SOL_TCP, AI_CANONNAME) canonical = tuples[0][3] for item in tuples: addresses.append(item[4][0]) # XXX we just ignore aliases return (canonical, aliases, addresses) def gethostbyname_ex(self, hostname, family=AF_INET): hostname = self._hostname_to_bytes(hostname) if hostname in self._LOCAL_AND_BROADCAST_HOSTNAMES: # The broadcast specials aren't handled here, but they may produce # special errors that are hard to replicate across all systems. return native_gethostbyname_ex(hostname) return self._gethostbyname_ex(hostname, family) def _getaddrinfo(self, host_bytes, port, family, socktype, proto, flags): raise NotImplementedError def getaddrinfo(self, host, port, family=0, socktype=0, proto=0, flags=0): host = self._hostname_to_bytes(host) if host is not None else None if ( not isinstance(host, bytes) # 1, 2 or (flags & AI_NUMERICHOST) # 3 or host in self._LOCAL_HOSTNAMES # 4 or (is_ipv6_addr(host) and host.startswith(b'fe80')) # 5 ): # 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 # 4) It's a well-known alias. TODO: This is special casing for c-ares that we don't # really want to do. It's here because it resolves a discrepancy with the system # resolvers caught by test cases. In gevent 20.4.0, this only worked correctly on # Python 3 and not Python 2, by accident. # 5) host is a link-local ipv6; dnspython returns the wrong # scope-id for those. return native_getaddrinfo(host, port, family, socktype, proto, flags) return self._getaddrinfo(host, port, family, socktype, proto, flags) def _getaliases(self, hostname, family): # pylint:disable=unused-argument return [] def _gethostbyaddr(self, ip_address_bytes): """Raises herror.""" raise NotImplementedError def gethostbyaddr(self, ip_address): ip_address = _resolve_special(ip_address, AF_UNSPEC) ip_address = self._hostname_to_bytes(ip_address) if ip_address in self._LOCAL_AND_BROADCAST_HOSTNAMES: return native_gethostbyaddr(ip_address) return self._gethostbyaddr(ip_address) def _getnameinfo(self, address_bytes, port, sockaddr, flags): raise NotImplementedError def getnameinfo(self, sockaddr, flags): if not isinstance(flags, integer_types): raise TypeError('an integer is required') if not isinstance(sockaddr, tuple): raise TypeError('getnameinfo() argument 1 must be a tuple') address = sockaddr[0] address = self._hostname_to_bytes(sockaddr[0]) if address in self._LOCAL_AND_BROADCAST_HOSTNAMES: return native_getnameinfo(sockaddr, flags) port = sockaddr[1] if not isinstance(port, integer_types): raise TypeError('port must be an integer, not %s' % type(port)) if not PYPY and port >= 65536: # System resolvers do different things with an # out-of-bound port; macOS CPython 3.8 raises ``gaierror: [Errno 8] # nodename nor servname provided, or not known``, while # manylinux CPython 2.7 appears to ignore it and raises ``error: # sockaddr resolved to multiple addresses``. TravisCI, at least ot # one point, successfully resolved www.gevent.org to ``(readthedocs.org, '0')``. # But c-ares 1.16 would raise ``gaierror(25, 'ARES_ESERVICE: unknown')``. # Doing this appears to get the expected results on CPython port = 0 if PYPY and (port < 0 or port >= 65536): # PyPy seems to always be strict about that and produce the same results # on all platforms. raise OverflowError("port must be 0-65535.") if len(sockaddr) > 2: # Must be IPv6: (host, port, [flowinfo, [scopeid]]) flowinfo = sockaddr[2] if flowinfo > 0xfffff: raise OverflowError("getnameinfo(): flowinfo must be 0-1048575.") return self._getnameinfo(address, port, sockaddr, flags)