[3.6] bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (GH-409) (#2062)

* bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (#409)

(cherry picked from commit a608d2d5a7f1aabe9bcbfc220135c5e126189390)

* [3.6] bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (GH-409)

* asyncio SSL contexts leak sockets after calling close with certain servers

* cleanup _shutdown_timeout_handle on _fatal_error.
(cherry picked from commit a608d2d5a7f1aabe9bcbfc220135c5e126189390)
diff --git a/Lib/asyncio/sslproto.py b/Lib/asyncio/sslproto.py
index 7948c4c..3b1eb99 100644
--- a/Lib/asyncio/sslproto.py
+++ b/Lib/asyncio/sslproto.py
@@ -7,6 +7,7 @@
 
 from . import base_events
 from . import compat
+from . import futures
 from . import protocols
 from . import transports
 from .log import logger
@@ -412,7 +413,7 @@
 
     def __init__(self, loop, app_protocol, sslcontext, waiter,
                  server_side=False, server_hostname=None,
-                 call_connection_made=True):
+                 call_connection_made=True, shutdown_timeout=5.0):
         if ssl is None:
             raise RuntimeError('stdlib ssl module not available')
 
@@ -443,6 +444,8 @@
         self._session_established = False
         self._in_handshake = False
         self._in_shutdown = False
+        self._shutdown_timeout = shutdown_timeout
+        self._shutdown_timeout_handle = None
         # transport, ex: SelectorSocketTransport
         self._transport = None
         self._call_connection_made = call_connection_made
@@ -557,6 +560,15 @@
             self._in_shutdown = True
             self._write_appdata(b'')
 
+        if self._shutdown_timeout is not None:
+            self._shutdown_timeout_handle = self._loop.call_later(
+                self._shutdown_timeout, self._on_shutdown_timeout)
+
+    def _on_shutdown_timeout(self):
+        if self._transport is not None:
+            self._fatal_error(
+                futures.TimeoutError(), 'Can not complete shitdown operation')
+
     def _write_appdata(self, data):
         self._write_backlog.append((data, 0))
         self._write_buffer_size += len(data)
@@ -684,12 +696,22 @@
             })
         if self._transport:
             self._transport._force_close(exc)
+            self._transport = None
+
+        if self._shutdown_timeout_handle is not None:
+            self._shutdown_timeout_handle.cancel()
+            self._shutdown_timeout_handle = None
 
     def _finalize(self):
         self._sslpipe = None
 
         if self._transport is not None:
             self._transport.close()
+            self._transport = None
+
+        if self._shutdown_timeout_handle is not None:
+            self._shutdown_timeout_handle.cancel()
+            self._shutdown_timeout_handle = None
 
     def _abort(self):
         try:
diff --git a/Lib/test/test_asyncio/test_sslproto.py b/Lib/test/test_asyncio/test_sslproto.py
index bcd236e..6bcaa9e 100644
--- a/Lib/test/test_asyncio/test_sslproto.py
+++ b/Lib/test/test_asyncio/test_sslproto.py
@@ -96,6 +96,40 @@
         test_utils.run_briefly(self.loop)
         self.assertIsInstance(waiter.exception(), ConnectionAbortedError)
 
+    def test_close_abort(self):
+        # From issue #bpo-29406
+        # abort connection if server does not complete shutdown procedure
+        ssl_proto = self.ssl_protocol()
+        transport = self.connection_made(ssl_proto)
+        ssl_proto._on_handshake_complete(None)
+        ssl_proto._start_shutdown()
+        self.assertIsNotNone(ssl_proto._shutdown_timeout_handle)
+
+        exc_handler = mock.Mock()
+        self.loop.set_exception_handler(exc_handler)
+        ssl_proto._shutdown_timeout_handle._run()
+
+        exc_handler.assert_called_with(
+            self.loop, {'message': 'Can not complete shitdown operation',
+                        'exception': mock.ANY,
+                        'transport': transport,
+                        'protocol': ssl_proto}
+        )
+        self.assertIsNone(ssl_proto._shutdown_timeout_handle)
+
+    def test_close(self):
+        # From issue #bpo-29406
+        # abort connection if server does not complete shutdown procedure
+        ssl_proto = self.ssl_protocol()
+        transport = self.connection_made(ssl_proto)
+        ssl_proto._on_handshake_complete(None)
+        ssl_proto._start_shutdown()
+        self.assertIsNotNone(ssl_proto._shutdown_timeout_handle)
+
+        ssl_proto._finalize()
+        self.assertIsNone(ssl_proto._transport)
+        self.assertIsNone(ssl_proto._shutdown_timeout_handle)
+
     def test_close_during_handshake(self):
         # bpo-29743 Closing transport during handshake process leaks socket
         waiter = asyncio.Future(loop=self.loop)