From c2d6343110c99e48aaaf98929b8b69b5fa6f0d58 Mon Sep 17 00:00:00 2001 From: Shane Harvey Date: Thu, 2 Jul 2020 14:17:20 -0700 Subject: [PATCH] PYTHON-2163 Suppress ragged EOFs when using pyOpenSSL to match the stdlib (#453) Wrap pyOpenSSL connection errors with AutoReconnect. --- pymongo/pool.py | 10 ++++++---- pymongo/pyopenssl_context.py | 30 ++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/pymongo/pool.py b/pymongo/pool.py index b04e4bd33..a18e005ba 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -1007,7 +1007,7 @@ def _configured_socket(address, options): # Raise CertificateError directly like we do after match_hostname # below. raise - except IOError as exc: + except (IOError, OSError, _SSLError) as exc: sock.close() # We raise AutoReconnect for transient and permanent SSL handshake # failures alike. Permanent handshake failures, like protocol @@ -1176,15 +1176,17 @@ class Pool: if self.enabled_for_cmap: listeners.publish_connection_created(self.address, conn_id) - sock = None try: sock = _configured_socket(self.address, self.opts) - except socket.error as error: + except Exception as error: if self.enabled_for_cmap: listeners.publish_connection_closed( self.address, conn_id, ConnectionClosedReason.ERROR) - _raise_connection_failure(self.address, error) + if isinstance(error, (IOError, OSError, _SSLError)): + _raise_connection_failure(self.address, error) + + raise sock_info = SocketInfo(sock, self, self.address, conn_id) if self.handshake: diff --git a/pymongo/pyopenssl_context.py b/pymongo/pyopenssl_context.py index bb10eb6a0..10c35141f 100644 --- a/pymongo/pyopenssl_context.py +++ b/pymongo/pyopenssl_context.py @@ -83,14 +83,20 @@ _RETRY_ERRORS = ( _SSL.WantReadError, _SSL.WantWriteError, _SSL.WantX509LookupError) +def _ragged_eof(exc): + """Return True if the OpenSSL.SSL.SysCallError is a ragged EOF.""" + return exc.args == (-1, 'Unexpected EOF') + + # https://github.com/pyca/pyopenssl/issues/168 # https://github.com/pyca/pyopenssl/issues/176 # https://docs.python.org/3/library/ssl.html#notes-on-non-blocking-sockets class _sslConn(_SSL.Connection): - def __init__(self, *args, **kwargs): + def __init__(self, ctx, sock, suppress_ragged_eofs): self.socket_checker = _SocketChecker() - super(_sslConn, self).__init__(*args, **kwargs) + self.suppress_ragged_eofs = suppress_ragged_eofs + super(_sslConn, self).__init__(ctx, sock) def _call(self, call, *args, **kwargs): timeout = self.gettimeout() @@ -110,10 +116,22 @@ class _sslConn(_SSL.Connection): return self._call(super(_sslConn, self).do_handshake, *args, **kwargs) def recv(self, *args, **kwargs): - return self._call(super(_sslConn, self).recv, *args, **kwargs) + try: + return self._call(super(_sslConn, self).recv, *args, **kwargs) + except _SSL.SysCallError as exc: + # Suppress ragged EOFs to match the stdlib. + if self.suppress_ragged_eofs and _ragged_eof(exc): + return b"" + raise def recv_into(self, *args, **kwargs): - return self._call(super(_sslConn, self).recv_into, *args, **kwargs) + try: + return self._call(super(_sslConn, self).recv_into, *args, **kwargs) + except _SSL.SysCallError as exc: + # Suppress ragged EOFs to match the stdlib. + if self.suppress_ragged_eofs and _ragged_eof(exc): + return 0 + raise def sendall(self, buf, flags=0): view = memoryview(buf) @@ -266,12 +284,12 @@ class SSLContext(object): def wrap_socket(self, sock, server_side=False, do_handshake_on_connect=True, - suppress_ragged_eofs=True, # TODO: Add support to _sslConn. + suppress_ragged_eofs=True, server_hostname=None, session=None): """Wrap an existing Python socket sock and return a TLS socket object. """ - ssl_conn = _sslConn(self._ctx, sock) + ssl_conn = _sslConn(self._ctx, sock, suppress_ragged_eofs) if session: ssl_conn.set_session(session) if server_side is True: