You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
10 KiB
Python
278 lines
10 KiB
Python
5 years ago
|
# 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'<broadcast>',
|
||
|
)
|
||
|
|
||
|
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 "<broadcast>" 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)
|