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.
940 lines
34 KiB
Python
940 lines
34 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
# pylint:disable=broad-except
|
|
import gevent
|
|
from gevent import monkey
|
|
|
|
import os
|
|
import re
|
|
|
|
import unittest
|
|
import socket
|
|
from time import time
|
|
import traceback
|
|
|
|
import gevent.socket as gevent_socket
|
|
import gevent.testing as greentest
|
|
|
|
from gevent.testing import util
|
|
from gevent.testing import six
|
|
from gevent.testing.six import xrange
|
|
from gevent.testing import flaky
|
|
from gevent.testing.skipping import skipWithoutExternalNetwork
|
|
|
|
|
|
resolver = gevent.get_hub().resolver
|
|
util.debug('Resolver: %s', resolver)
|
|
|
|
if getattr(resolver, 'pool', None) is not None:
|
|
resolver.pool.size = 1
|
|
|
|
from gevent.testing.sysinfo import RESOLVER_NOT_SYSTEM
|
|
from gevent.testing.sysinfo import RESOLVER_DNSPYTHON
|
|
from gevent.testing.sysinfo import RESOLVER_ARES
|
|
from gevent.testing.sysinfo import PY2
|
|
from gevent.testing.sysinfo import PYPY
|
|
import gevent.testing.timing
|
|
|
|
|
|
assert gevent_socket.gaierror is socket.gaierror
|
|
assert gevent_socket.error is socket.error
|
|
|
|
|
|
RUN_ALL_HOST_TESTS = os.getenv('GEVENTTEST_RUN_ALL_ETC_HOST_TESTS', '')
|
|
|
|
|
|
def compare_relaxed(a, b):
|
|
"""
|
|
>>> compare_relaxed('2a00:1450:400f:801::1010', '2a00:1450:400f:800::1011')
|
|
True
|
|
|
|
>>> compare_relaxed('2a00:1450:400f:801::1010', '2aXX:1450:400f:900::1011')
|
|
False
|
|
|
|
>>> compare_relaxed('2a00:1450:4016:800::1013', '2a00:1450:4008:c01::93')
|
|
True
|
|
|
|
>>> compare_relaxed('2001:470::e852:4a38:9d7f:0', '2001:470:6d00:1c:1::d00')
|
|
True
|
|
|
|
>>> compare_relaxed('2001:470:4147:4943:6161:6161:2e74:6573', '2001:470::')
|
|
True
|
|
|
|
>>> compare_relaxed('2607:f8b0:6708:24af:1fd:700:60d4:4af', '2607:f8b0:2d00::f000:0')
|
|
True
|
|
|
|
>>> compare_relaxed('a.google.com', 'b.google.com')
|
|
True
|
|
|
|
>>> compare_relaxed('a.google.com', 'a.gevent.org')
|
|
False
|
|
"""
|
|
# IPv6 address from different requests might be different
|
|
a_segments = a.count(':')
|
|
b_segments = b.count(':')
|
|
if a_segments and b_segments:
|
|
if a_segments == b_segments and a_segments in (4, 5, 6, 7):
|
|
return True
|
|
if a.rstrip(':').startswith(b.rstrip(':')) or b.rstrip(':').startswith(a.rstrip(':')):
|
|
return True
|
|
if a_segments >= 2 and b_segments >= 2 and a.split(':')[:2] == b.split(':')[:2]:
|
|
return True
|
|
|
|
return a.split('.', 1)[-1] == b.split('.', 1)[-1]
|
|
|
|
|
|
def contains_5tuples(lst):
|
|
for item in lst:
|
|
if not (isinstance(item, tuple) and len(item) == 5):
|
|
return False
|
|
return True
|
|
|
|
|
|
def relaxed_is_equal(a, b):
|
|
"""
|
|
>>> relaxed_is_equal([(10, 1, 6, '', ('2a00:1450:400f:801::1010', 80, 0, 0))], [(10, 1, 6, '', ('2a00:1450:400f:800::1011', 80, 0, 0))])
|
|
True
|
|
|
|
>>> relaxed_is_equal([1, '2'], (1, '2'))
|
|
False
|
|
|
|
>>> relaxed_is_equal([1, '2'], [1, '2'])
|
|
True
|
|
|
|
>>> relaxed_is_equal(('wi-in-x93.1e100.net', 'http'), ('we-in-x68.1e100.net', 'http'))
|
|
True
|
|
"""
|
|
if type(a) is not type(b):
|
|
return False
|
|
if a == b:
|
|
return True
|
|
if isinstance(a, six.string_types):
|
|
return compare_relaxed(a, b)
|
|
try:
|
|
if len(a) != len(b):
|
|
return False
|
|
except TypeError:
|
|
return False
|
|
if contains_5tuples(a) and contains_5tuples(b):
|
|
# getaddrinfo results
|
|
a = sorted(a)
|
|
b = sorted(b)
|
|
return all(relaxed_is_equal(x, y) for (x, y) in zip(a, b))
|
|
|
|
|
|
def add(klass, hostname, name=None,
|
|
skip=None, skip_reason=None):
|
|
|
|
call = callable(hostname)
|
|
|
|
def _setattr(k, n, func):
|
|
if skip:
|
|
func = greentest.skipIf(skip, skip_reason,)(func)
|
|
if not hasattr(k, n):
|
|
setattr(k, n, func)
|
|
|
|
if name is None:
|
|
if call:
|
|
name = hostname.__name__
|
|
else:
|
|
name = re.sub(r'[^\w]+', '_', repr(hostname))
|
|
assert name, repr(hostname)
|
|
|
|
def test_getaddrinfo_http(self):
|
|
x = hostname() if call else hostname
|
|
self._test('getaddrinfo', x, 'http')
|
|
test_getaddrinfo_http.__name__ = 'test_%s_getaddrinfo_http' % name
|
|
_setattr(klass, test_getaddrinfo_http.__name__, test_getaddrinfo_http)
|
|
|
|
def test_gethostbyname(self):
|
|
x = hostname() if call else hostname
|
|
ipaddr = self._test('gethostbyname', x)
|
|
if not isinstance(ipaddr, Exception):
|
|
self._test('gethostbyaddr', ipaddr)
|
|
test_gethostbyname.__name__ = 'test_%s_gethostbyname' % name
|
|
_setattr(klass, test_gethostbyname.__name__, test_gethostbyname)
|
|
|
|
def test3(self):
|
|
x = hostname() if call else hostname
|
|
self._test('gethostbyname_ex', x)
|
|
test3.__name__ = 'test_%s_gethostbyname_ex' % name
|
|
_setattr(klass, test3.__name__, test3)
|
|
|
|
def test4(self):
|
|
x = hostname() if call else hostname
|
|
self._test('gethostbyaddr', x)
|
|
test4.__name__ = 'test_%s_gethostbyaddr' % name
|
|
_setattr(klass, test4.__name__, test4)
|
|
|
|
def test5(self):
|
|
x = hostname() if call else hostname
|
|
self._test('getnameinfo', (x, 80), 0)
|
|
test5.__name__ = 'test_%s_getnameinfo' % name
|
|
_setattr(klass, test5.__name__, test5)
|
|
|
|
@skipWithoutExternalNetwork("Tries to resolve and compare hostnames/addrinfo")
|
|
class TestCase(greentest.TestCase):
|
|
maxDiff = None
|
|
__timeout__ = 30
|
|
switch_expected = None
|
|
|
|
TRACE = not util.QUIET and os.getenv('GEVENT_DEBUG', '') == 'trace'
|
|
verbose_dns = TRACE
|
|
|
|
def trace(self, message, *args, **kwargs):
|
|
if self.TRACE:
|
|
util.debug(message, *args, **kwargs)
|
|
|
|
# Things that the stdlib should never raise and neither should we;
|
|
# these indicate bugs in our code and we want to raise them.
|
|
REAL_ERRORS = (AttributeError, ValueError, NameError)
|
|
|
|
def __run_resolver(self, function, args):
|
|
try:
|
|
result = function(*args)
|
|
assert not isinstance(result, BaseException), repr(result)
|
|
return result
|
|
except self.REAL_ERRORS:
|
|
raise
|
|
except Exception as ex:
|
|
if self.TRACE:
|
|
traceback.print_exc()
|
|
return ex
|
|
|
|
def __trace_call(self, result, runtime, function, *args):
|
|
util.debug(self.__format_call(function, args))
|
|
self.__trace_fresult(result, runtime)
|
|
|
|
def __format_call(self, function, args):
|
|
args = repr(args)
|
|
if args.endswith(',)'):
|
|
args = args[:-2] + ')'
|
|
try:
|
|
module = function.__module__.replace('gevent._socketcommon', 'gevent')
|
|
name = function.__name__
|
|
return '%s:%s%s' % (module, name, args)
|
|
except AttributeError:
|
|
return function + args
|
|
|
|
def __trace_fresult(self, result, seconds):
|
|
if isinstance(result, Exception):
|
|
msg = ' -=> raised %r' % (result, )
|
|
else:
|
|
msg = ' -=> returned %r' % (result, )
|
|
time_ms = ' %.2fms' % (seconds * 1000.0, )
|
|
space = 80 - len(msg) - len(time_ms)
|
|
if space > 0:
|
|
space = ' ' * space
|
|
else:
|
|
space = ''
|
|
util.debug(msg + space + time_ms)
|
|
|
|
if not TRACE:
|
|
def run_resolver(self, function, func_args):
|
|
now = time()
|
|
return self.__run_resolver(function, func_args), time() - now
|
|
else:
|
|
def run_resolver(self, function, func_args):
|
|
self.trace(self.__format_call(function, func_args))
|
|
delta = time()
|
|
result = self.__run_resolver(function, func_args)
|
|
delta = time() - delta
|
|
self.__trace_fresult(result, delta)
|
|
return result, delta
|
|
|
|
def setUp(self):
|
|
super(TestCase, self).setUp()
|
|
if not self.verbose_dns:
|
|
# Silence the default reporting of errors from the ThreadPool,
|
|
# we handle those here.
|
|
gevent.get_hub().exception_stream = None
|
|
|
|
def tearDown(self):
|
|
if not self.verbose_dns:
|
|
try:
|
|
del gevent.get_hub().exception_stream
|
|
except AttributeError:
|
|
pass # Happens under leak tests
|
|
super(TestCase, self).tearDown()
|
|
|
|
def should_log_results(self, result1, result2):
|
|
if not self.verbose_dns:
|
|
return False
|
|
|
|
if isinstance(result1, BaseException) and isinstance(result2, BaseException):
|
|
return type(result1) is not type(result2)
|
|
return repr(result1) != repr(result2)
|
|
|
|
def _test(self, func_name, *args):
|
|
"""
|
|
Runs the function *func_name* with *args* and compares gevent and the system.
|
|
|
|
Returns the gevent result.
|
|
"""
|
|
gevent_func = getattr(gevent_socket, func_name)
|
|
real_func = monkey.get_original('socket', func_name)
|
|
|
|
tester = getattr(self, '_run_test_' + func_name, self._run_test_generic)
|
|
result = tester(func_name, real_func, gevent_func, args)
|
|
_real_result, time_real, gevent_result, time_gevent = result
|
|
|
|
if self.verbose_dns and time_gevent > time_real + 0.02 and time_gevent > 0.03:
|
|
msg = 'gevent:%s%s took %dms versus %dms stdlib' % (
|
|
func_name, args, time_gevent * 1000.0, time_real * 1000.0)
|
|
|
|
if time_gevent > time_real + 1:
|
|
word = 'VERY'
|
|
else:
|
|
word = 'quite'
|
|
|
|
util.log('\nWARNING: %s slow: %s', word, msg, color='warning')
|
|
|
|
return gevent_result
|
|
|
|
def _run_test_generic(self, func_name, real_func, gevent_func, func_args):
|
|
real_result, time_real = self.run_resolver(real_func, func_args)
|
|
gevent_result, time_gevent = self.run_resolver(gevent_func, func_args)
|
|
if util.QUIET and self.should_log_results(real_result, gevent_result):
|
|
util.log('')
|
|
self.__trace_call(real_result, time_real, real_func, func_args)
|
|
self.__trace_call(gevent_result, time_gevent, gevent_func, func_args)
|
|
self.assertEqualResults(real_result, gevent_result, func_name)
|
|
return real_result, time_real, gevent_result, time_gevent
|
|
|
|
def _normalize_result(self, result, func_name):
|
|
norm_name = '_normalize_result_' + func_name
|
|
if hasattr(self, norm_name):
|
|
return getattr(self, norm_name)(result)
|
|
return result
|
|
|
|
|
|
NORMALIZE_GAI_IGNORE_CANONICAL_NAME = RESOLVER_ARES # It tends to return them even when not asked for
|
|
if not RESOLVER_NOT_SYSTEM:
|
|
def _normalize_result_getaddrinfo(self, result):
|
|
return result
|
|
def _normalize_result_gethostbyname_ex(self, result):
|
|
return result
|
|
else:
|
|
def _normalize_result_gethostbyname_ex(self, result):
|
|
# Often the second and third part of the tuple (hostname, aliaslist, ipaddrlist)
|
|
# can be in different orders if we're hitting different servers,
|
|
# or using the native and ares resolvers due to load-balancing techniques.
|
|
# We sort them.
|
|
if isinstance(result, BaseException):
|
|
return result
|
|
# result[1].sort() # we wind up discarding this
|
|
|
|
# On Py2 in test_russion_gethostbyname_ex, this
|
|
# is actually an integer, for some reason. In TestLocalhost.tets__ip6_localhost,
|
|
# the result isn't this long (maybe an error?).
|
|
try:
|
|
result[2].sort()
|
|
except AttributeError:
|
|
pass
|
|
except IndexError:
|
|
return result
|
|
# On some systems, a random alias is found in the aliaslist
|
|
# by the system resolver, but not by cares, and vice versa. We deem the aliaslist
|
|
# unimportant and discard it.
|
|
# On some systems (Travis CI), the ipaddrlist for 'localhost' can come back
|
|
# with two entries 127.0.0.1 (presumably two interfaces?) for c-ares
|
|
ips = result[2]
|
|
if ips == ['127.0.0.1', '127.0.0.1']:
|
|
ips = ['127.0.0.1']
|
|
# On some systems, the hostname can get caps
|
|
return (result[0].lower(), [], ips)
|
|
|
|
def _normalize_result_getaddrinfo(self, result):
|
|
# Result is a list
|
|
# (family, socktype, proto, canonname, sockaddr)
|
|
# e.g.,
|
|
# (AF_INET, SOCK_STREAM, IPPROTO_TCP, 'readthedocs.io', (127.0.0.1, 80))
|
|
if isinstance(result, BaseException):
|
|
return result
|
|
|
|
# On Python 3, the builtin resolver can return SOCK_RAW results, but
|
|
# c-ares doesn't do that. So we remove those if we find them.
|
|
# Likewise, on certain Linux systems, even on Python 2, IPPROTO_SCTP (132)
|
|
# results may be returned --- but that may not even have a constant in the
|
|
# socket module! So to be safe, we strip out anything that's not
|
|
# SOCK_STREAM or SOCK_DGRAM
|
|
if isinstance(result, list):
|
|
result = [
|
|
x
|
|
for x in result
|
|
if x[1] in (socket.SOCK_STREAM, socket.SOCK_DGRAM)
|
|
and x[2] in (socket.IPPROTO_TCP, socket.IPPROTO_UDP)
|
|
]
|
|
|
|
if self.NORMALIZE_GAI_IGNORE_CANONICAL_NAME:
|
|
result = [
|
|
(family, kind, proto, '', addr)
|
|
for family, kind, proto, _, addr
|
|
in result
|
|
]
|
|
|
|
if isinstance(result, list):
|
|
result.sort()
|
|
return result
|
|
|
|
def _normalize_result_getnameinfo(self, result):
|
|
return result
|
|
|
|
NORMALIZE_GHBA_IGNORE_ALIAS = False
|
|
def _normalize_result_gethostbyaddr(self, result):
|
|
if not RESOLVER_NOT_SYSTEM:
|
|
return result
|
|
|
|
if self.NORMALIZE_GHBA_IGNORE_ALIAS and isinstance(result, tuple):
|
|
# On some systems, a random alias is found in the aliaslist
|
|
# by the system resolver, but not by cares and vice versa. This is *probably* only the
|
|
# case for localhost or things otherwise in /etc/hosts. We deem the aliaslist
|
|
# unimportant and discard it.
|
|
return (result[0], [], result[2])
|
|
return result
|
|
|
|
def _compare_exceptions(self, real_result, gevent_result, func_name):
|
|
msg = (func_name, 'system:', repr(real_result), 'gevent:', repr(gevent_result))
|
|
self.assertIs(type(gevent_result), type(real_result), msg)
|
|
if isinstance(real_result, TypeError):
|
|
return
|
|
|
|
if PYPY and isinstance(real_result, socket.herror):
|
|
# PyPy doesn't do errno or multiple arguments in herror;
|
|
# it just puts a string like 'host lookup failed: <thehost>';
|
|
# it must be doing that manually.
|
|
return
|
|
|
|
self.assertEqual(real_result.args, gevent_result.args, msg)
|
|
if hasattr(real_result, 'errno'):
|
|
self.assertEqual(real_result.errno, gevent_result.errno)
|
|
|
|
def assertEqualResults(self, real_result, gevent_result, func_name):
|
|
errors = (socket.gaierror, socket.herror, TypeError)
|
|
if isinstance(real_result, errors) and isinstance(gevent_result, errors):
|
|
self._compare_exceptions(real_result, gevent_result, func_name)
|
|
return
|
|
|
|
real_result = self._normalize_result(real_result, func_name)
|
|
gevent_result = self._normalize_result(gevent_result, func_name)
|
|
|
|
real_result_repr = repr(real_result)
|
|
gevent_result_repr = repr(gevent_result)
|
|
if real_result_repr == gevent_result_repr:
|
|
return
|
|
if relaxed_is_equal(gevent_result, real_result):
|
|
return
|
|
|
|
# If we're using a different resolver, allow the real resolver to generate an
|
|
# error that the gevent resolver actually gets an answer to.
|
|
if (
|
|
RESOLVER_NOT_SYSTEM
|
|
and isinstance(real_result, errors)
|
|
and not isinstance(gevent_result, errors)
|
|
):
|
|
return
|
|
|
|
# On PyPy, socket.getnameinfo() can produce results even when the hostname resolves to
|
|
# multiple addresses, like www.gevent.org does. DNSPython (and c-ares?) don't do that,
|
|
# they refuse to pick a name and raise ``socket.error``
|
|
if (
|
|
RESOLVER_NOT_SYSTEM
|
|
and PYPY
|
|
and func_name == 'getnameinfo'
|
|
and isinstance(gevent_result, socket.error)
|
|
and not isinstance(real_result, socket.error)
|
|
):
|
|
return
|
|
|
|
|
|
# From 2.7 on, assertEqual does a better job highlighting the results than we would
|
|
# because it calls assertSequenceEqual, which highlights the exact
|
|
# difference in the tuple
|
|
self.assertEqual(real_result, gevent_result)
|
|
|
|
|
|
class TestTypeError(TestCase):
|
|
pass
|
|
|
|
add(TestTypeError, None)
|
|
add(TestTypeError, 25)
|
|
|
|
|
|
class TestHostname(TestCase):
|
|
NORMALIZE_GHBA_IGNORE_ALIAS = True
|
|
|
|
def __normalize_name(self, result):
|
|
if (RESOLVER_ARES or RESOLVER_DNSPYTHON) and isinstance(result, tuple):
|
|
# The system resolver can return the FQDN, in the first result,
|
|
# when given certain configurations. But c-ares and dnspython
|
|
# do not.
|
|
name = result[0]
|
|
name = name.split('.', 1)[0]
|
|
result = (name,) + result[1:]
|
|
return result
|
|
|
|
def _normalize_result_gethostbyaddr(self, result):
|
|
result = TestCase._normalize_result_gethostbyaddr(self, result)
|
|
return self.__normalize_name(result)
|
|
|
|
def _normalize_result_getnameinfo(self, result):
|
|
result = TestCase._normalize_result_getnameinfo(self, result)
|
|
if PY2:
|
|
# Not sure why we only saw this on Python 2
|
|
result = self.__normalize_name(result)
|
|
return result
|
|
|
|
add(
|
|
TestHostname,
|
|
socket.gethostname,
|
|
skip=greentest.RUNNING_ON_TRAVIS and greentest.RESOLVER_NOT_SYSTEM,
|
|
skip_reason=("Sometimes get a different result for getaddrinfo "
|
|
"with dnspython; c-ares produces different results for "
|
|
"localhost on Travis beginning Sept 2019")
|
|
)
|
|
|
|
|
|
class TestLocalhost(TestCase):
|
|
# certain tests in test_patched_socket.py only work if getaddrinfo('localhost') does not switch
|
|
# (e.g. NetworkConnectionAttributesTest.testSourceAddress)
|
|
#switch_expected = False
|
|
# XXX: The above has been commented out for some time. Apparently this isn't the case
|
|
# anymore.
|
|
|
|
def _normalize_result_getaddrinfo(self, result):
|
|
if RESOLVER_NOT_SYSTEM:
|
|
# We see that some impls (OS X) return extra results
|
|
# like DGRAM that ares does not.
|
|
return ()
|
|
return super(TestLocalhost, self)._normalize_result_getaddrinfo(result)
|
|
|
|
NORMALIZE_GHBA_IGNORE_ALIAS = True
|
|
if greentest.RUNNING_ON_TRAVIS and greentest.PY2 and RESOLVER_NOT_SYSTEM:
|
|
def _normalize_result_gethostbyaddr(self, result):
|
|
# Beginning in November 2017 after an upgrade to Travis,
|
|
# we started seeing ares return ::1 for localhost, but
|
|
# the system resolver is still returning 127.0.0.1 under Python 2
|
|
result = super(TestLocalhost, self)._normalize_result_gethostbyaddr(result)
|
|
if isinstance(result, tuple):
|
|
result = (result[0], result[1], ['127.0.0.1'])
|
|
return result
|
|
|
|
|
|
add(
|
|
TestLocalhost, 'ip6-localhost',
|
|
skip=RESOLVER_DNSPYTHON, # XXX: Fix these.
|
|
skip_reason="Can return gaierror(-2)"
|
|
)
|
|
add(
|
|
TestLocalhost, 'localhost',
|
|
skip=greentest.RUNNING_ON_TRAVIS,
|
|
skip_reason="Can return gaierror(-2)"
|
|
)
|
|
|
|
def dnspython_lenient_compare_exceptions(self, real_result, gevent_result, func_name):
|
|
try:
|
|
TestCase._compare_exceptions(self, real_result, gevent_result, func_name)
|
|
except AssertionError:
|
|
# Allow gethostbyaddr to raise different things in a few rare cases.
|
|
if (
|
|
func_name != 'gethostbyaddr'
|
|
or type(real_result) not in (socket.herror, socket.gaierror)
|
|
or type(gevent_result) not in (socket.herror, socket.gaierror)
|
|
):
|
|
raise
|
|
util.log('WARNING: error type mismatch for %s: %r (gevent) != %r (stdlib)',
|
|
func_name,
|
|
gevent_result, real_result,
|
|
color='warning')
|
|
|
|
|
|
class TestNonexistent(TestCase):
|
|
if RESOLVER_DNSPYTHON:
|
|
_compare_exceptions = dnspython_lenient_compare_exceptions
|
|
|
|
|
|
add(TestNonexistent, 'nonexistentxxxyyy')
|
|
|
|
|
|
class Test1234(TestCase):
|
|
pass
|
|
|
|
add(Test1234, '1.2.3.4')
|
|
|
|
|
|
class Test127001(TestCase):
|
|
NORMALIZE_GHBA_IGNORE_ALIAS = True
|
|
|
|
add(
|
|
Test127001, '127.0.0.1',
|
|
# skip=RESOLVER_DNSPYTHON,
|
|
# skip_reason="Beginning Dec 1 2017, ares started returning ip6-localhost "
|
|
# "instead of localhost"
|
|
)
|
|
|
|
|
|
|
|
class TestBroadcast(TestCase):
|
|
switch_expected = False
|
|
|
|
if RESOLVER_DNSPYTHON:
|
|
# dnspython raises errors for broadcasthost/255.255.255.255, but the system
|
|
# can resolve it.
|
|
|
|
@unittest.skip('ares raises errors for broadcasthost/255.255.255.255')
|
|
def test__broadcast__gethostbyaddr(self):
|
|
return
|
|
|
|
test__broadcast__gethostbyname = test__broadcast__gethostbyaddr
|
|
|
|
add(TestBroadcast, '<broadcast>')
|
|
|
|
|
|
from gevent.resolver._hostsfile import HostsFile
|
|
class SanitizedHostsFile(HostsFile):
|
|
def iter_all_host_addr_pairs(self):
|
|
for name, addr in super(SanitizedHostsFile, self).iter_all_host_addr_pairs():
|
|
if (RESOLVER_NOT_SYSTEM
|
|
and (name.endswith('local') # ignore bonjour, ares can't find them
|
|
# ignore common aliases that ares can't find
|
|
or addr == '255.255.255.255'
|
|
or name == 'broadcasthost'
|
|
# We get extra results from some impls, like OS X
|
|
# it returns DGRAM results
|
|
or name == 'localhost')):
|
|
continue # pragma: no cover
|
|
if name.endswith('local'):
|
|
# These can only be found if bonjour is running,
|
|
# and are very slow to do so with the system resolver on OS X
|
|
continue
|
|
yield name, addr
|
|
|
|
|
|
@greentest.skipIf(greentest.RUNNING_ON_CI,
|
|
"This sometimes randomly fails on Travis with ares and on appveyor, beginning Feb 13, 2018")
|
|
# Probably due to round-robin DNS,
|
|
# since this is not actually the system's etc hosts file.
|
|
# TODO: Rethink this. We need something reliable. Go back to using
|
|
# the system's etc hosts?
|
|
class TestEtcHosts(TestCase):
|
|
|
|
MAX_HOSTS = int(os.getenv('GEVENTTEST_MAX_ETC_HOSTS', '10'))
|
|
|
|
@classmethod
|
|
def populate_tests(cls):
|
|
hf = SanitizedHostsFile(os.path.join(os.path.dirname(__file__),
|
|
'hosts_file.txt'))
|
|
all_etc_hosts = sorted(hf.iter_all_host_addr_pairs())
|
|
if len(all_etc_hosts) > cls.MAX_HOSTS and not RUN_ALL_HOST_TESTS:
|
|
all_etc_hosts = all_etc_hosts[:cls.MAX_HOSTS]
|
|
|
|
for host, ip in all_etc_hosts:
|
|
add(cls, host)
|
|
add(cls, ip)
|
|
|
|
|
|
|
|
TestEtcHosts.populate_tests()
|
|
|
|
|
|
|
|
class TestGeventOrg(TestCase):
|
|
# For this test to work correctly, it needs to resolve to
|
|
# an address with a single A record; round-robin DNS and multiple A records
|
|
# may mess it up (subsequent requests---and we always make two---may return
|
|
# unequal results). We used to use gevent.org, but that now has multiple A records;
|
|
# trying www.gevent.org which is a CNAME to readthedocs.org then worked, but it became
|
|
# an alias for python-gevent.readthedocs.org, which is an alias for readthedocs.io,
|
|
# and which also has multiple addresses. So we run the resolver twice to try to get
|
|
# the different answers, if needed.
|
|
HOSTNAME = 'www.gevent.org'
|
|
|
|
|
|
if RESOLVER_NOT_SYSTEM:
|
|
def _normalize_result_gethostbyname(self, result):
|
|
if result == '104.17.33.82':
|
|
result = '104.17.32.82'
|
|
return result
|
|
|
|
def _normalize_result_gethostbyname_ex(self, result):
|
|
result = super(TestGeventOrg, self)._normalize_result_gethostbyname_ex(result)
|
|
if result[0] == 'python-gevent.readthedocs.org':
|
|
result = ('readthedocs.io', ) + result[1:]
|
|
return result
|
|
|
|
def test_AI_CANONNAME(self):
|
|
# Not all systems support AI_CANONNAME; notably tha manylinux
|
|
# resolvers *sometimes* do not. Specifically, sometimes they
|
|
# provide the canonical name *only* on the first result.
|
|
|
|
args = (
|
|
# host
|
|
TestGeventOrg.HOSTNAME,
|
|
# port
|
|
None,
|
|
# family
|
|
socket.AF_INET,
|
|
# type
|
|
0,
|
|
# proto
|
|
0,
|
|
# flags
|
|
socket.AI_CANONNAME
|
|
)
|
|
gevent_result = gevent_socket.getaddrinfo(*args)
|
|
self.assertEqual(gevent_result[0][3], 'readthedocs.io')
|
|
real_result = socket.getaddrinfo(*args)
|
|
|
|
self.NORMALIZE_GAI_IGNORE_CANONICAL_NAME = not all(r[3] for r in real_result)
|
|
try:
|
|
self.assertEqualResults(real_result, gevent_result, 'getaddrinfo')
|
|
finally:
|
|
del self.NORMALIZE_GAI_IGNORE_CANONICAL_NAME
|
|
|
|
add(TestGeventOrg, TestGeventOrg.HOSTNAME)
|
|
|
|
|
|
class TestFamily(TestCase):
|
|
def test_inet(self):
|
|
self._test('getaddrinfo', TestGeventOrg.HOSTNAME, None, socket.AF_INET)
|
|
|
|
def test_unspec(self):
|
|
self._test('getaddrinfo', TestGeventOrg.HOSTNAME, None, socket.AF_UNSPEC)
|
|
|
|
def test_badvalue(self):
|
|
self._test('getaddrinfo', TestGeventOrg.HOSTNAME, None, 255)
|
|
self._test('getaddrinfo', TestGeventOrg.HOSTNAME, None, 255000)
|
|
self._test('getaddrinfo', TestGeventOrg.HOSTNAME, None, -1)
|
|
|
|
@unittest.skipIf(RESOLVER_DNSPYTHON, "Raises the wrong errno")
|
|
def test_badtype(self):
|
|
self._test('getaddrinfo', TestGeventOrg.HOSTNAME, 'x')
|
|
|
|
|
|
class Test_getaddrinfo(TestCase):
|
|
|
|
def _test_getaddrinfo(self, *args):
|
|
self._test('getaddrinfo', *args)
|
|
|
|
def test_80(self):
|
|
self._test_getaddrinfo(TestGeventOrg.HOSTNAME, 80)
|
|
|
|
def test_int_string(self):
|
|
self._test_getaddrinfo(TestGeventOrg.HOSTNAME, '80')
|
|
|
|
def test_0(self):
|
|
self._test_getaddrinfo(TestGeventOrg.HOSTNAME, 0)
|
|
|
|
def test_http(self):
|
|
self._test_getaddrinfo(TestGeventOrg.HOSTNAME, 'http')
|
|
|
|
def test_notexistent_tld(self):
|
|
self._test_getaddrinfo('myhost.mytld', 53)
|
|
|
|
def test_notexistent_dot_com(self):
|
|
self._test_getaddrinfo('sdfsdfgu5e66098032453245wfdggd.com', 80)
|
|
|
|
def test1(self):
|
|
return self._test_getaddrinfo(TestGeventOrg.HOSTNAME, 52, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, 0)
|
|
|
|
def test2(self):
|
|
return self._test_getaddrinfo(TestGeventOrg.HOSTNAME, 53, socket.AF_INET, socket.SOCK_DGRAM, 17)
|
|
|
|
@unittest.skipIf(RESOLVER_DNSPYTHON,
|
|
"dnspython only returns some of the possibilities")
|
|
def test3(self):
|
|
return self._test_getaddrinfo('google.com', 'http', socket.AF_INET6)
|
|
|
|
|
|
@greentest.skipIf(PY2, "Enums only on Python 3.4+")
|
|
def test_enums(self):
|
|
# https://github.com/gevent/gevent/issues/1310
|
|
|
|
# On Python 3, getaddrinfo does special things to make sure that
|
|
# the fancy enums are returned.
|
|
|
|
gai = gevent_socket.getaddrinfo('example.com', 80,
|
|
socket.AF_INET,
|
|
socket.SOCK_STREAM, socket.IPPROTO_TCP)
|
|
af, socktype, _proto, _canonname, _sa = gai[0]
|
|
self.assertIs(socktype, socket.SOCK_STREAM)
|
|
self.assertIs(af, socket.AF_INET)
|
|
|
|
class TestInternational(TestCase):
|
|
if PY2:
|
|
# We expect these to raise UnicodeEncodeError, which is a
|
|
# subclass of ValueError
|
|
REAL_ERRORS = set(TestCase.REAL_ERRORS) - {ValueError,}
|
|
|
|
if RESOLVER_ARES:
|
|
|
|
def test_russian_getaddrinfo_http(self):
|
|
# And somehow, test_russion_getaddrinfo_http (``getaddrinfo(name, 'http')``)
|
|
# manages to work with recent versions of Python 2, but our preemptive encoding
|
|
# to ASCII causes it to fail with the c-ares resolver; but only that one test out of
|
|
# all of them.
|
|
self.skipTest("ares fails to encode.")
|
|
|
|
|
|
# dns python can actually resolve these: it uses
|
|
# the 2008 version of idna encoding, whereas on Python 2,
|
|
# with the default resolver, it tries to encode to ascii and
|
|
# raises a UnicodeEncodeError. So we get different results.
|
|
add(TestInternational, u'президент.рф', 'russian',
|
|
skip=(PY2 and RESOLVER_DNSPYTHON),
|
|
skip_reason="dnspython can actually resolve these")
|
|
add(TestInternational, u'президент.рф'.encode('idna'), 'idna')
|
|
|
|
@skipWithoutExternalNetwork("Tries to resolve and compare hostnames/addrinfo")
|
|
class TestInterrupted_gethostbyname(gevent.testing.timing.AbstractGenericWaitTestCase):
|
|
|
|
# There are refs to a Waiter in the C code that don't go
|
|
# away yet; one gc may or may not do it.
|
|
@greentest.ignores_leakcheck
|
|
def test_returns_none_after_timeout(self):
|
|
super(TestInterrupted_gethostbyname, self).test_returns_none_after_timeout()
|
|
|
|
def wait(self, timeout):
|
|
with gevent.Timeout(timeout, False):
|
|
for index in xrange(1000000):
|
|
try:
|
|
gevent_socket.gethostbyname('www.x%s.com' % index)
|
|
except socket.error:
|
|
pass
|
|
raise AssertionError('Timeout was not raised')
|
|
|
|
def cleanup(self):
|
|
# Depending on timing, this can raise:
|
|
# (This suddenly started happening on Apr 6 2016; www.x1000000.com
|
|
# is apparently no longer around)
|
|
|
|
# File "test__socket_dns.py", line 538, in cleanup
|
|
# gevent.get_hub().threadpool.join()
|
|
# File "/home/travis/build/gevent/gevent/src/gevent/threadpool.py", line 108, in join
|
|
# sleep(delay)
|
|
# File "/home/travis/build/gevent/gevent/src/gevent/hub.py", line 169, in sleep
|
|
# hub.wait(loop.timer(seconds, ref=ref))
|
|
# File "/home/travis/build/gevent/gevent/src/gevent/hub.py", line 651, in wait
|
|
# result = waiter.get()
|
|
# File "/home/travis/build/gevent/gevent/src/gevent/hub.py", line 899, in get
|
|
# return self.hub.switch()
|
|
# File "/home/travis/build/gevent/gevent/src/greentest/greentest.py", line 520, in switch
|
|
# return _original_Hub.switch(self, *args)
|
|
# File "/home/travis/build/gevent/gevent/src/gevent/hub.py", line 630, in switch
|
|
# return RawGreenlet.switch(self)
|
|
# gaierror: [Errno -2] Name or service not known
|
|
try:
|
|
gevent.get_hub().threadpool.join()
|
|
except Exception: # pragma: no cover pylint:disable=broad-except
|
|
traceback.print_exc()
|
|
|
|
|
|
# class TestInterrupted_getaddrinfo(greentest.GenericWaitTestCase):
|
|
#
|
|
# def wait(self, timeout):
|
|
# with gevent.Timeout(timeout, False):
|
|
# for index in range(1000):
|
|
# try:
|
|
# gevent_socket.getaddrinfo('www.a%s.com' % index, 'http')
|
|
# except socket.gaierror:
|
|
# pass
|
|
|
|
|
|
class TestBadName(TestCase):
|
|
if RESOLVER_DNSPYTHON:
|
|
_compare_exceptions = dnspython_lenient_compare_exceptions
|
|
|
|
|
|
add(TestBadName, 'xxxxxxxxxxxx')
|
|
|
|
class TestBadIP(TestCase):
|
|
|
|
if RESOLVER_DNSPYTHON:
|
|
_compare_exceptions = dnspython_lenient_compare_exceptions
|
|
|
|
|
|
add(TestBadIP, '1.2.3.400')
|
|
|
|
|
|
@greentest.skipIf(greentest.RUNNING_ON_TRAVIS, "Travis began returning ip6-localhost")
|
|
class Test_getnameinfo_127001(TestCase):
|
|
|
|
def test(self):
|
|
self._test('getnameinfo', ('127.0.0.1', 80), 0)
|
|
|
|
def test_DGRAM(self):
|
|
self._test('getnameinfo', ('127.0.0.1', 779), 0)
|
|
self._test('getnameinfo', ('127.0.0.1', 779), socket.NI_DGRAM)
|
|
|
|
def test_NOFQDN(self):
|
|
# I get ('localhost', 'www') with _socket but ('localhost.localdomain', 'www') with gevent.socket
|
|
self._test('getnameinfo', ('127.0.0.1', 80), socket.NI_NOFQDN)
|
|
|
|
def test_NAMEREQD(self):
|
|
self._test('getnameinfo', ('127.0.0.1', 80), socket.NI_NAMEREQD)
|
|
|
|
|
|
class Test_getnameinfo_geventorg(TestCase):
|
|
|
|
@unittest.skipIf(RESOLVER_DNSPYTHON,
|
|
"dnspython raises an error when multiple results are returned")
|
|
def test_NUMERICHOST(self):
|
|
self._test('getnameinfo', (TestGeventOrg.HOSTNAME, 80), 0)
|
|
self._test('getnameinfo', (TestGeventOrg.HOSTNAME, 80), socket.NI_NUMERICHOST)
|
|
|
|
@unittest.skipIf(RESOLVER_DNSPYTHON,
|
|
"dnspython raises an error when multiple results are returned")
|
|
def test_NUMERICSERV(self):
|
|
self._test('getnameinfo', (TestGeventOrg.HOSTNAME, 80), socket.NI_NUMERICSERV)
|
|
|
|
def test_domain1(self):
|
|
self._test('getnameinfo', (TestGeventOrg.HOSTNAME, 80), 0)
|
|
|
|
def test_domain2(self):
|
|
self._test('getnameinfo', ('www.gevent.org', 80), 0)
|
|
|
|
def test_port_zero(self):
|
|
self._test('getnameinfo', ('www.gevent.org', 0), 0)
|
|
|
|
|
|
class Test_getnameinfo_fail(TestCase):
|
|
|
|
def test_port_string(self):
|
|
self._test('getnameinfo', ('www.gevent.org', 'http'), 0)
|
|
|
|
def test_bad_flags(self):
|
|
self._test('getnameinfo', ('localhost', 80), 55555555)
|
|
|
|
|
|
class TestInvalidPort(TestCase):
|
|
|
|
@flaky.reraises_flaky_race_condition()
|
|
def test_overflow_neg_one(self):
|
|
# An Appveyor beginning 2019-03-21, the system resolver
|
|
# sometimes returns ('23.100.69.251', '65535') instead of
|
|
# raising an error. That IP address belongs to
|
|
# readthedocs[.io?] which is where www.gevent.org is a CNAME
|
|
# to...but it doesn't actually *reverse* to readthedocs.io.
|
|
# Can't reproduce locally, not sure what's happening
|
|
self._test('getnameinfo', ('www.gevent.org', -1), 0)
|
|
|
|
# Beginning with PyPy 2.7 7.1 on Appveyor, we sometimes see this
|
|
# return an OverflowError instead of the TypeError about None
|
|
@greentest.skipOnLibuvOnPyPyOnWin("Errors dont match")
|
|
def test_typeerror_none(self):
|
|
self._test('getnameinfo', ('www.gevent.org', None), 0)
|
|
|
|
# Beginning with PyPy 2.7 7.1 on Appveyor, we sometimes see this
|
|
# return an TypeError instead of the OverflowError.
|
|
# XXX: But see Test_getnameinfo_fail.test_port_string where this does work.
|
|
@greentest.skipOnLibuvOnPyPyOnWin("Errors don't match")
|
|
def test_typeerror_str(self):
|
|
self._test('getnameinfo', ('www.gevent.org', 'x'), 0)
|
|
|
|
def test_overflow_port_too_large(self):
|
|
self._test('getnameinfo', ('www.gevent.org', 65536), 0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
greentest.main()
|