Commit 3bd798e7 authored by Jason Madden's avatar Jason Madden Committed by GitHub

Merge pull request #1635 from gevent/issue1634

Pass addresses through inet_pton before getaddrinfo
parents f19927e8 9184feec
...@@ -4,9 +4,13 @@ shallow_clone: true ...@@ -4,9 +4,13 @@ shallow_clone: true
build: build:
parallel: true parallel: true
verbosity: minimal verbosity: minimal
# The VS 2019 image doesn't have
# the MSVC needed for Python 2.7.
image: Visual Studio 2015
environment: environment:
global: global:
APPVEYOR_SAVE_CACHE_ON_ERROR: "true"
# SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the
# /E:ON and /V:ON options are not enabled in the batch script interpreter # /E:ON and /V:ON options are not enabled in the batch script interpreter
# See: http://stackoverflow.com/a/13751649/163740 # See: http://stackoverflow.com/a/13751649/163740
...@@ -125,7 +129,7 @@ install: ...@@ -125,7 +129,7 @@ install:
- ps: "ls \"C:/\"" - ps: "ls \"C:/\""
- ECHO "Installed SDKs:" - ECHO "Installed SDKs:"
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\"" - ps: "if(Test-Path(\"C:/Program Files/Microsoft SDKs/Windows\")) {ls \"C:/Program Files/Microsoft SDKs/Windows\";}"
# Install Python (from the official .msi of http://python.org) and pip when # Install Python (from the official .msi of http://python.org) and pip when
# not already installed. # not already installed.
...@@ -167,7 +171,7 @@ install: ...@@ -167,7 +171,7 @@ install:
cache: cache:
- "%TMP%\\py\\" - "%TMP%\\py\\"
- '%LOCALAPPDATA%\pip\Cache' - '%LOCALAPPDATA%\pip\Cache -> appveyor.yml,setup.py'
build_script: build_script:
# Build the compiled extension # Build the compiled extension
......
``gevent.socket.create_connection`` and
``gevent.socket.socket.connect`` no longer ignore IPv6 scope IDs.
Any IP address (IPv4 or IPv6) is no longer subject to an extra call to
``getaddrinfo``. Depending on the resolver in use, this is likely to
change the number and order of greenlet switches. (On Windows, in
particular test cases when there are no other greenlets running, it has
been observed to lead to ``LoopExit`` in scenarios that didn't produce
that before.)
...@@ -251,6 +251,14 @@ class socket(_socketcommon.SocketMixin): ...@@ -251,6 +251,14 @@ class socket(_socketcommon.SocketMixin):
return isinstance(self._sock, _closedsocket) return isinstance(self._sock, _closedsocket)
def connect(self, address): def connect(self, address):
"""
Connect to *address*.
.. versionchanged:: NEXT
If the host part of the address includes an IPv6 scope ID,
it will be used instead of ignored, if the platform supplies
:func:`socket.inet_pton`.
"""
if self.timeout == 0.0: if self.timeout == 0.0:
return self._sock.connect(address) return self._sock.connect(address)
......
...@@ -389,10 +389,17 @@ class socket(_socketcommon.SocketMixin): ...@@ -389,10 +389,17 @@ class socket(_socketcommon.SocketMixin):
return sock.detach() return sock.detach()
def connect(self, address): def connect(self, address):
"""
Connect to *address*.
.. versionchanged:: NEXT
If the host part of the address includes an IPv6 scope ID,
it will be used instead of ignored, if the platform supplies
:func:`socket.inet_pton`.
"""
if self.timeout == 0.0: if self.timeout == 0.0:
return _socket.socket.connect(self._sock, address) return _socket.socket.connect(self._sock, address)
address = _socketcommon._resolve_addr(self._sock, address) address = _socketcommon._resolve_addr(self._sock, address)
with Timeout._start_new_or_dummy(self.timeout, timeout("timed out")): with Timeout._start_new_or_dummy(self.timeout, timeout("timed out")):
while True: while True:
err = self.getsockopt(SOL_SOCKET, SO_ERROR) err = self.getsockopt(SOL_SOCKET, SO_ERROR)
......
...@@ -398,6 +398,20 @@ def _resolve_addr(sock, address): ...@@ -398,6 +398,20 @@ def _resolve_addr(sock, address):
if sock.family not in _RESOLVABLE_FAMILIES or not isinstance(address, tuple): if sock.family not in _RESOLVABLE_FAMILIES or not isinstance(address, tuple):
return address return address
# address is (host, port) (ipv4) or (host, port, flowinfo, scopeid) (ipv6). # address is (host, port) (ipv4) or (host, port, flowinfo, scopeid) (ipv6).
# If it's already resolved, no need to go through getaddrinfo() again.
# That can lose precision (e.g., on IPv6, it can lose scopeid). The standard library
# does this in socketmodule.c:setipaddr. (This is only part of the logic, the real
# thing is much more complex.)
try:
if __socket__.inet_pton(sock.family, address[0]):
return address
except AttributeError: # pragma: no cover
# inet_pton might not be available.
pass
except __socket__.error:
# Not parseable, needs resolved.
pass
# We don't pass the port to getaddrinfo because the C # We don't pass the port to getaddrinfo because the C
# socket module doesn't either (on some systems its # socket module doesn't either (on some systems its
......
...@@ -806,5 +806,5 @@ def get_server_certificate(addr, ssl_version=PROTOCOL_SSLv23, ca_certs=None): ...@@ -806,5 +806,5 @@ def get_server_certificate(addr, ssl_version=PROTOCOL_SSLv23, ca_certs=None):
with wrap_socket(sock, ssl_version=ssl_version, with wrap_socket(sock, ssl_version=ssl_version,
cert_reqs=cert_reqs, ca_certs=ca_certs) as sslsock: cert_reqs=cert_reqs, ca_certs=ca_certs) as sslsock:
dercert = sslsock.getpeercert(True) dercert = sslsock.getpeercert(True)
sslsock = sock = None
return DER_cert_to_PEM_cert(dercert) return DER_cert_to_PEM_cert(dercert)
...@@ -75,6 +75,11 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=N ...@@ -75,6 +75,11 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=N
must be a tuple of (host, port) for the socket to bind as a source must be a tuple of (host, port) for the socket to bind as a source
address before making the connection. A host of '' or port 0 tells address before making the connection. A host of '' or port 0 tells
the OS to use the default. the OS to use the default.
.. versionchanged:: NEXT
If the host part of the address includes an IPv6 scope ID,
it will be used instead of ignored, if the platform supplies
:func:`socket.inet_pton`.
""" """
host, port = address host, port = address
...@@ -85,7 +90,7 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=N ...@@ -85,7 +90,7 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=N
raise error("getaddrinfo returns an empty list") raise error("getaddrinfo returns an empty list")
for res in addrs: for res in addrs:
af, socktype, proto, _, sa = res af, socktype, proto, _canonname, sa = res
sock = None sock = None
try: try:
sock = socket(af, socktype, proto) sock = socket(af, socktype, proto)
......
...@@ -703,6 +703,8 @@ if WIN: ...@@ -703,6 +703,8 @@ if WIN:
disabled_tests += [ disabled_tests += [
# Issue with Unix vs DOS newlines in the file vs from the server # Issue with Unix vs DOS newlines in the file vs from the server
'test_ssl.ThreadedTests.test_socketserver', 'test_ssl.ThreadedTests.test_socketserver',
# This sometimes hangs (only on appveyor)
'test_ssl.ThreadedTests.test_asyncore_server',
# On appveyor, this sometimes produces 'A non-blocking socket # On appveyor, this sometimes produces 'A non-blocking socket
# operation could not be completed immediately', followed by # operation could not be completed immediately', followed by
# 'No connection could be made because the target machine # 'No connection could be made because the target machine
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import errno
import os import os
import sys import sys
...@@ -125,7 +125,9 @@ if RUNNING_ON_APPVEYOR: ...@@ -125,7 +125,9 @@ if RUNNING_ON_APPVEYOR:
EXPECT_POOR_TIMER_RESOLUTION = ( EXPECT_POOR_TIMER_RESOLUTION = (
PYPY3 PYPY3
or RUNNING_ON_APPVEYOR # Really, this is probably only in VMs. But that's all I test
# Windows with.
or WIN
or (LIBUV and PYPY) or (LIBUV and PYPY)
or RUN_COVERAGE or RUN_COVERAGE
or (OSX and RUNNING_ON_CI) or (OSX and RUNNING_ON_CI)
...@@ -133,16 +135,19 @@ EXPECT_POOR_TIMER_RESOLUTION = ( ...@@ -133,16 +135,19 @@ EXPECT_POOR_TIMER_RESOLUTION = (
CONN_ABORTED_ERRORS = [] CONN_ABORTED_ERRORS = []
try: def _make_socket_errnos(*names):
from errno import WSAECONNABORTED result = []
CONN_ABORTED_ERRORS.append(WSAECONNABORTED) for name in names:
except ImportError: try:
pass x = getattr(errno, name)
except AttributeError:
from errno import ECONNRESET pass
CONN_ABORTED_ERRORS.append(ECONNRESET) else:
result.append(x)
CONN_ABORTED_ERRORS = frozenset(CONN_ABORTED_ERRORS) return frozenset(result)
CONN_ABORTED_ERRORS = _make_socket_errnos('WSAECONNABORTED', 'ECONNRESET')
CONN_REFUSED_ERRORS = _make_socket_errnos('WSAECONNREFUSED', 'ECONNREFUSED')
RESOLVER_ARES = os.getenv('GEVENT_RESOLVER') == 'ares' RESOLVER_ARES = os.getenv('GEVENT_RESOLVER') == 'ares'
RESOLVER_DNSPYTHON = os.getenv('GEVENT_RESOLVER') == 'dnspython' RESOLVER_DNSPYTHON = os.getenv('GEVENT_RESOLVER') == 'dnspython'
......
...@@ -8,10 +8,14 @@ import os ...@@ -8,10 +8,14 @@ import os
import gevent.testing as greentest import gevent.testing as greentest
from gevent.testing import PY3 from gevent.testing import PY3
from gevent.testing import DEFAULT_SOCKET_TIMEOUT as _DEFAULT_SOCKET_TIMEOUT from gevent.testing import DEFAULT_SOCKET_TIMEOUT as _DEFAULT_SOCKET_TIMEOUT
from gevent.testing.timing import SMALLEST_RELIABLE_DELAY
from gevent.testing.sockets import tcp_listener from gevent.testing.sockets import tcp_listener
from gevent.testing import WIN
from gevent import socket from gevent import socket
import gevent import gevent
from gevent.server import StreamServer from gevent.server import StreamServer
from gevent.exceptions import LoopExit
class SimpleStreamServer(StreamServer): class SimpleStreamServer(StreamServer):
...@@ -40,6 +44,17 @@ class SimpleStreamServer(StreamServer): ...@@ -40,6 +44,17 @@ class SimpleStreamServer(StreamServer):
finally: finally:
fd.close() fd.close()
def sleep_to_clear_old_sockets(*_args):
try:
# Allow any queued callbacks needed to close sockets
# to run. On Windows, this needs to spin the event loop to
# allow proper FD cleanup. Otherwise we risk getting an
# old FD that's being closed and then get spurious connection
# errors.
gevent.sleep(0 if not WIN else SMALLEST_RELIABLE_DELAY)
except Exception: # pylint:disable=broad-except
pass
class Settings(object): class Settings(object):
ServerClass = StreamServer ServerClass = StreamServer
...@@ -50,7 +65,10 @@ class Settings(object): ...@@ -50,7 +65,10 @@ class Settings(object):
@staticmethod @staticmethod
def assertAcceptedConnectionError(inst): def assertAcceptedConnectionError(inst):
with inst.makefile() as conn: with inst.makefile() as conn:
result = conn.read() try:
result = conn.read()
except socket.timeout:
result = None
inst.assertFalse(result) inst.assertFalse(result)
assert500 = assertAcceptedConnectionError assert500 = assertAcceptedConnectionError
...@@ -86,6 +104,7 @@ class TestCase(greentest.TestCase): ...@@ -86,6 +104,7 @@ class TestCase(greentest.TestCase):
if getattr(self, 'server', None) is not None: if getattr(self, 'server', None) is not None:
self.server.stop() self.server.stop()
self.server = None self.server = None
sleep_to_clear_old_sockets()
def get_listener(self): def get_listener(self):
return self._close_on_teardown(tcp_listener(backlog=5)) return self._close_on_teardown(tcp_listener(backlog=5))
...@@ -139,10 +158,21 @@ class TestCase(greentest.TestCase): ...@@ -139,10 +158,21 @@ class TestCase(greentest.TestCase):
# A kernel bug in OS X sometimes results in this # A kernel bug in OS X sometimes results in this
LOCAL_CONN_REFUSED_ERRORS = (errno.EPROTOTYPE,) LOCAL_CONN_REFUSED_ERRORS = (errno.EPROTOTYPE,)
def assertConnectionRefused(self): def assertConnectionRefused(self, in_proc_server=True):
with self.assertRaises(socket.error) as exc: try:
with self.makefile() as conn: with self.assertRaises(socket.error) as exc:
conn.close() with self.makefile() as conn:
conn.close()
except LoopExit:
if not in_proc_server:
raise
# A LoopExit is fine. If we've killed the server
# and don't have any other greenlets to run, then
# blocking to open the connection might raise this.
# This became likely on Windows once we stopped
# passing IP addresses through an extra call to
# ``getaddrinfo``, which changed the number of switches
return
ex = exc.exception ex = exc.exception
self.assertIn(ex.args[0], self.assertIn(ex.args[0],
...@@ -163,19 +193,23 @@ class TestCase(greentest.TestCase): ...@@ -163,19 +193,23 @@ class TestCase(greentest.TestCase):
self.Settings.assertPoolFull(self) self.Settings.assertPoolFull(self)
def assertNotAccepted(self): def assertNotAccepted(self):
with self.makefile(include_raw_socket=True) as (conn, sock): try:
conn.write(b'GET / HTTP/1.0\r\n\r\n') with self.makefile(include_raw_socket=True) as (conn, sock):
conn.flush() conn.write(b'GET / HTTP/1.0\r\n\r\n')
result = b'' conn.flush()
try: result = b''
while True: try:
data = sock.recv(1) while True:
if not data: data = sock.recv(1)
break if not data:
result += data break
except socket.timeout: result += data
self.assertFalse(result) except socket.timeout:
return self.assertFalse(result)
return
except LoopExit:
# See assertConnectionRefused
return
self.assertTrue(result.startswith(b'HTTP/1.0 500 Internal Server Error'), repr(result)) self.assertTrue(result.startswith(b'HTTP/1.0 500 Internal Server Error'), repr(result))
...@@ -207,7 +241,7 @@ class TestCase(greentest.TestCase): ...@@ -207,7 +241,7 @@ class TestCase(greentest.TestCase):
def init_server(self): def init_server(self):
self.server = self._create_server() self.server = self._create_server()
self.server.start() self.server.start()
gevent.sleep() sleep_to_clear_old_sockets()
@property @property
def socket(self): def socket(self):
...@@ -258,6 +292,7 @@ class TestDefaultSpawn(TestCase): ...@@ -258,6 +292,7 @@ class TestDefaultSpawn(TestCase):
self.assertNotAccepted() self.assertNotAccepted()
self.server.start_accepting() self.server.start_accepting()
self.report_netstat('after start_accepting') self.report_netstat('after start_accepting')
sleep_to_clear_old_sockets()
self.assertRequestSucceeded() self.assertRequestSucceeded()
self.stop_server() self.stop_server()
self.report_netstat('after stop') self.report_netstat('after stop')
...@@ -276,6 +311,7 @@ class TestDefaultSpawn(TestCase): ...@@ -276,6 +311,7 @@ class TestDefaultSpawn(TestCase):
self.server = self.ServerSubClass(self.get_listener()) self.server = self.ServerSubClass(self.get_listener())
self.assertNotAccepted() self.assertNotAccepted()
@greentest.skipOnAppVeyor("Sometimes times out.")
def test_subclass_with_socket(self): def test_subclass_with_socket(self):
self.server = self.ServerSubClass(self.get_listener()) self.server = self.ServerSubClass(self.get_listener())
# the connection won't be refused, because there exists a # the connection won't be refused, because there exists a
...@@ -295,7 +331,7 @@ class TestDefaultSpawn(TestCase): ...@@ -295,7 +331,7 @@ class TestDefaultSpawn(TestCase):
def _test_serve_forever(self): def _test_serve_forever(self):
g = gevent.spawn(self.server.serve_forever) g = gevent.spawn(self.server.serve_forever)
try: try:
gevent.sleep(0.01) sleep_to_clear_old_sockets()
self.assertRequestSucceeded() self.assertRequestSucceeded()
self.server.stop() self.server.stop()
self.assertFalse(self.server.started) self.assertFalse(self.server.started)
...@@ -319,9 +355,11 @@ class TestDefaultSpawn(TestCase): ...@@ -319,9 +355,11 @@ class TestDefaultSpawn(TestCase):
self.assertTrue(self.server.started) self.assertTrue(self.server.started)
self._test_serve_forever() self._test_serve_forever()
@greentest.skipIf(greentest.EXPECT_POOR_TIMER_RESOLUTION, "Sometimes spuriously fails")
def test_server_closes_client_sockets(self): def test_server_closes_client_sockets(self):
self.server = self.ServerClass((greentest.DEFAULT_BIND_ADDR, 0), lambda *args: []) self.server = self.ServerClass((greentest.DEFAULT_BIND_ADDR, 0), lambda *args: [])
self.server.start() self.server.start()
sleep_to_clear_old_sockets()
with self.makefile() as conn: with self.makefile() as conn:
self.send_request_to_fd(conn) self.send_request_to_fd(conn)
# use assert500 below? # use assert500 below?
...@@ -330,6 +368,8 @@ class TestDefaultSpawn(TestCase): ...@@ -330,6 +368,8 @@ class TestDefaultSpawn(TestCase):
result = conn.read() result = conn.read()
if result: if result:
assert result.startswith('HTTP/1.0 500 Internal Server Error'), repr(result) assert result.startswith('HTTP/1.0 500 Internal Server Error'), repr(result)
except socket.timeout:
pass
except socket.error as ex: except socket.error as ex:
if ex.args[0] == 10053: if ex.args[0] == 10053:
pass # "established connection was aborted by the software in your host machine" pass # "established connection was aborted by the software in your host machine"
...@@ -348,7 +388,9 @@ class TestDefaultSpawn(TestCase): ...@@ -348,7 +388,9 @@ class TestDefaultSpawn(TestCase):
self.init_server() self.init_server()
self.assertTrue(self.server.started) self.assertTrue(self.server.started)
error = ExpectedError('test_error_in_spawn') error = ExpectedError('test_error_in_spawn')
self.server._spawn = lambda *args: gevent.getcurrent().throw(error) def _spawn(*_args):
gevent.getcurrent().throw(error)
self.server._spawn = _spawn
self.expect_one_error() self.expect_one_error()
self.assertAcceptedConnectionError() self.assertAcceptedConnectionError()
self.assert_error(ExpectedError, error) self.assert_error(ExpectedError, error)
...@@ -427,7 +469,7 @@ class TestNoneSpawn(TestCase): ...@@ -427,7 +469,7 @@ class TestNoneSpawn(TestCase):
def test_assertion_in_blocking_func(self): def test_assertion_in_blocking_func(self):
def sleep(*_args): def sleep(*_args):
gevent.sleep(0) gevent.sleep(SMALLEST_RELIABLE_DELAY)
self.server = self.Settings.ServerClass((greentest.DEFAULT_BIND_ADDR, 0), sleep, spawn=None) self.server = self.Settings.ServerClass((greentest.DEFAULT_BIND_ADDR, 0), sleep, spawn=None)
self.server.start() self.server.start()
self.expect_one_error() self.expect_one_error()
......
...@@ -562,6 +562,19 @@ class TestFunctions(greentest.TestCase): ...@@ -562,6 +562,19 @@ class TestFunctions(greentest.TestCase):
exclude.append('gethostbyaddr') exclude.append('gethostbyaddr')
self.assertMonkeyPatchedFuncSignatures('socket', exclude=exclude) self.assertMonkeyPatchedFuncSignatures('socket', exclude=exclude)
def test_resolve_ipv6_scope_id(self):
from gevent import _socketcommon as SC
if not SC.__socket__.has_ipv6:
self.skipTest("Needs IPv6") # pragma: no cover
if not hasattr(SC.__socket__, 'inet_pton'):
self.skipTest("Needs inet_pton") # pragma: no cover
# A valid IPv6 address, with a scope.
addr = ('2607:f8b0:4000:80e::200e', 80, 0, 9)
# Mock socket
class sock(object):
family = SC.AF_INET6 # pylint:disable=no-member
self.assertIs(addr, SC._resolve_addr(sock, addr))
class TestSocket(greentest.TestCase): class TestSocket(greentest.TestCase):
......
...@@ -21,12 +21,10 @@ ...@@ -21,12 +21,10 @@
import gevent.testing as greentest import gevent.testing as greentest
from gevent.testing import support from gevent.testing import support
from gevent.socket import socket, error from gevent.testing import sysinfo
try: from gevent.socket import socket, error
from errno import WSAECONNREFUSED as ECONNREFUSED from gevent.exceptions import LoopExit
except ImportError:
from errno import ECONNREFUSED
class TestSocketErrors(greentest.TestCase): class TestSocketErrors(greentest.TestCase):
...@@ -35,15 +33,15 @@ class TestSocketErrors(greentest.TestCase): ...@@ -35,15 +33,15 @@ class TestSocketErrors(greentest.TestCase):
def test_connection_refused(self): def test_connection_refused(self):
port = support.find_unused_port() port = support.find_unused_port()
s = socket() with socket() as s:
self._close_on_teardown(s) try:
try: with self.assertRaises(error) as exc:
s.connect((greentest.DEFAULT_CONNECT_HOST, port)) s.connect((greentest.DEFAULT_CONNECT_HOST, port))
except error as ex: except LoopExit:
self.assertEqual(ex.args[0], ECONNREFUSED, ex) return
self.assertIn('refused', str(ex).lower()) ex = exc.exception
else: self.assertIn(ex.args[0], sysinfo.CONN_REFUSED_ERRORS, ex)
self.fail("Shouldn't have connected") self.assertIn('refused', str(ex).lower())
if __name__ == '__main__': if __name__ == '__main__':
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment